diff --git a/.dockerignore b/.dockerignore index 3eed0c9..108e94a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -226,3 +226,10 @@ temp_*.sql !workers/detail-renderer/package.json !workers/detail-renderer/package-lock.json !public/detail-generator/fonts/OFL.txt + +# Isolated migration tooling, locked to the application's dependency versions. +/workers/database-migrator/node_modules +/workers/database-migrator/node_modules/** +!workers/database-migrator/package.json +!workers/database-migrator/package-lock.json +!scripts/database-migrator-lock.mjs diff --git a/Dockerfile b/Dockerfile index 43e1c2b..91dafbc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -156,6 +156,22 @@ RUN echo "清理 standalone 目录..." && \ rm -rf .next/standalone/scripts && \ echo "清理完成" +# Install only the migration/backup/seed dependency closure. Its lock is derived +# from the application lock and checked before installation to prevent drift. +FROM base AS migrator-deps +ARG ALPINE_MIRROR +WORKDIR /app +RUN sed -i "s|https://dl-cdn.alpinelinux.org/alpine|${ALPINE_MIRROR}|g" /etc/apk/repositories && \ + apk add --no-cache openssl +COPY package-lock.json ./package-lock.json +COPY workers/database-migrator/package.json workers/database-migrator/package-lock.json ./workers/database-migrator/ +COPY scripts/database-migrator-lock.mjs ./scripts/database-migrator-lock.mjs +RUN node scripts/database-migrator-lock.mjs --check +# The matching generated client is copied from builder; do not generate a second +# client during installation. The CLI engines still run their normal postinstall. +RUN --mount=type=cache,target=/root/.npm \ + cd workers/database-migrator && PRISMA_SKIP_POSTINSTALL_GENERATE=1 npm ci --no-audit --no-fund + # Explicit, one-shot database migrator. It is never used by the application # container or the normal deploy entry. The operator must supply production DB # identity, exact source SHA and a hashed backup receipt before --apply works. @@ -171,20 +187,23 @@ RUN sed -i "s|https://dl-cdn.alpinelinux.org/alpine|${ALPINE_MIRROR}|g" /etc/apk command -v psql >/dev/null ARG VCS_REF LABEL org.opencontainers.image.revision=$VCS_REF -COPY --from=builder /app/node_modules ./node_modules -COPY --from=builder /app/prisma ./prisma -COPY --from=builder /app/scripts/safe-production-migrate.mjs ./scripts/safe-production-migrate.mjs -COPY --from=builder /app/scripts/create-verified-production-backup.mjs ./scripts/create-verified-production-backup.mjs -COPY --from=builder /app/scripts/seed-team-function-config.ts ./scripts/seed-team-function-config.ts -COPY --from=builder /app/lib ./lib -COPY --from=builder /app/tsconfig.json ./tsconfig.json -COPY --from=builder /app/data/team-function-initial-content.json ./data/team-function-initial-content.json -COPY --from=builder /app/DEPLOYMENT_COMMIT /app/DEPLOYMENT_COMMIT +COPY --from=migrator-deps --chown=root:root /app/workers/database-migrator/node_modules ./node_modules +COPY --from=builder --chown=root:root /app/node_modules/.prisma ./node_modules/.prisma +COPY --from=builder --chown=root:root /app/prisma ./prisma +COPY --from=builder --chown=root:root /app/scripts/safe-production-migrate.mjs ./scripts/safe-production-migrate.mjs +COPY --from=builder --chown=root:root /app/scripts/create-verified-production-backup.mjs ./scripts/create-verified-production-backup.mjs +COPY --from=builder --chown=root:root /app/scripts/seed-team-function-config.ts ./scripts/seed-team-function-config.ts +# Source closure for the team-function seed; no application routes or services. +COPY --from=builder --chown=root:root /app/lib/auto-personnel-rules.ts /app/lib/organization-material-permissions.ts /app/lib/prisma.ts /app/lib/team-function-contract.ts /app/lib/department-permission.ts /app/lib/employee-directory.ts /app/lib/env-validation.ts /app/lib/material-type-mapper.ts /app/lib/material-type-options.ts /app/lib/permission-groups.ts /app/lib/team-functions.ts /app/lib/user-permission-inherit.ts /app/lib/video-material-types.ts ./lib/ +COPY --from=builder --chown=root:root /app/tsconfig.json ./tsconfig.json +COPY --from=builder --chown=root:root /app/data/team-function-initial-content.json ./data/team-function-initial-content.json +COPY --from=builder --chown=root:root /app/DEPLOYMENT_COMMIT /app/DEPLOYMENT_COMMIT RUN test "$(cat /app/DEPLOYMENT_COMMIT)" = "$VCS_REF" && \ - test -x /app/node_modules/prisma/node_modules/@prisma/engines/schema-engine-linux-musl && \ - chown -R node:node /app/node_modules/prisma/node_modules/@prisma/engines && \ - chmod -R a+rX,a-w /app/prisma /app/scripts /app/DEPLOYMENT_COMMIT + test -x /app/node_modules/prisma/node_modules/@prisma/engines/schema-engine-linux-musl-openssl-3.0.x && \ + test "$(stat -c '%a' /app/DEPLOYMENT_COMMIT)" = 444 USER node +# Prove the CLI can use the bundled native engine without downloading or writing. +RUN --network=none ./node_modules/.bin/prisma -v ENTRYPOINT ["node", "scripts/safe-production-migrate.mjs"] # 生产运行阶段 @@ -210,9 +229,9 @@ RUN addgroup --system --gid 1001 nodejs && \ RUN mkdir -p /app/scripts # 复制必要文件 -COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ -COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static -COPY --from=builder --chown=nextjs:nodejs /app/app ./app +COPY --from=builder --chown=root:root /app/.next/standalone ./ +COPY --from=builder --chown=root:root /app/.next/static ./.next/static +COPY --from=builder --chown=root:root /app/app ./app COPY --from=builder --chown=root:root /app/public ./public COPY --from=builder --chown=root:root /app/DEPLOYMENT_COMMIT /app/DEPLOYMENT_COMMIT COPY --from=detail-renderer-deps --chown=root:root /opt/detail-renderer /opt/detail-renderer @@ -221,15 +240,15 @@ ENV DETAIL_RENDERER_NODE=/opt/detail-renderer/bin/node ENV DETAIL_RENDERER_ENTRY=/opt/detail-renderer/cli.mjs # 只复制运行时必需的 node_modules -COPY --from=builder --chown=nextjs:nodejs /app/node_modules/.prisma ./node_modules/.prisma -COPY --from=builder --chown=nextjs:nodejs /app/node_modules/ag-psd ./node_modules/ag-psd -COPY --from=builder --chown=nextjs:nodejs /app/node_modules/ioredis ./node_modules/ioredis -COPY --from=builder --chown=nextjs:nodejs /app/node_modules/@ioredis ./node_modules/@ioredis -COPY --from=builder --chown=nextjs:nodejs /app/node_modules/@aws-sdk ./node_modules/@aws-sdk -COPY --from=builder --chown=nextjs:nodejs /app/node_modules/@smithy ./node_modules/@smithy +COPY --from=builder --chown=root:root /app/node_modules/.prisma ./node_modules/.prisma +COPY --from=builder --chown=root:root /app/node_modules/ag-psd ./node_modules/ag-psd +COPY --from=builder --chown=root:root /app/node_modules/ioredis ./node_modules/ioredis +COPY --from=builder --chown=root:root /app/node_modules/@ioredis ./node_modules/@ioredis +COPY --from=builder --chown=root:root /app/node_modules/@aws-sdk ./node_modules/@aws-sdk +COPY --from=builder --chown=root:root /app/node_modules/@smithy ./node_modules/@smithy # 复制运行时 schema,以及切流前只读校验所需的精确 migration;仍不复制 engine-cache。 -COPY --from=builder --chown=nextjs:nodejs /app/prisma/schema.prisma ./prisma/schema.prisma +COPY --from=builder --chown=root:root /app/prisma/schema.prisma ./prisma/schema.prisma COPY --from=builder --chown=root:root /app/prisma/migrations/20251118000000_add_upload_id_to_materials ./prisma/migrations/20251118000000_add_upload_id_to_materials COPY --from=builder --chown=root:root /app/prisma/migrations/20260811_add_video_transcode_lease ./prisma/migrations/20260811_add_video_transcode_lease COPY --from=builder --chown=root:root /app/prisma/migrations/20260811_update_brand_logo_defaults ./prisma/migrations/20260811_update_brand_logo_defaults @@ -265,29 +284,17 @@ COPY --from=builder --chown=root:root /app/prisma/migrations/20260924_add_certif COPY --from=builder --chown=root:root /app/prisma/migrations/20261012_detail_generator ./prisma/migrations/20261012_detail_generator COPY --from=builder --chown=root:root /app/prisma/migrations/20261012_ai_model_registry ./prisma/migrations/20261012_ai_model_registry COPY --from=builder --chown=root:root /app/prisma/migrations/20261013_ai_model_capabilities ./prisma/migrations/20261013_ai_model_capabilities -COPY --from=builder --chown=nextjs:nodejs /app/lib ./lib -COPY --from=builder --chown=nextjs:nodejs /app/scripts/public-traffic-rollup-from-nginx.py ./scripts/public-traffic-rollup-from-nginx.py -COPY --from=builder --chown=nextjs:nodejs /app/scripts/collapse-performance-survey-template-versions.mjs ./scripts/collapse-performance-survey-template-versions.mjs +COPY --from=builder --chown=root:root /app/lib ./lib +COPY --from=builder --chown=root:root /app/scripts/public-traffic-rollup-from-nginx.py ./scripts/public-traffic-rollup-from-nginx.py +COPY --from=builder --chown=root:root /app/scripts/collapse-performance-survey-template-versions.mjs ./scripts/collapse-performance-survey-template-versions.mjs -# 创建必要目录并收紧权限。 -# 应用代码归 root 所有,运行账号只能写上传目录和 Next.js 运行时缓存; -# 即使上层文件路径校验再次出现缺陷,也不能覆盖 server.js 或业务源码。 -RUN mkdir -p /app/public /app/uploads && \ - mkdir -p /app/detail-generator-private && \ - mkdir -p /app/scripts && \ - mkdir -p /app/.next/cache && \ - mkdir -p /app/uploads/temp && \ - mkdir -p /app/uploads/assembled && \ - mkdir -p /app/uploads/materials && \ - mkdir -p /app/uploads/cleanup && \ - chown -R root:root /app && \ - chmod -R a+rX /app/public /app/prisma/migrations /app/scripts && \ - chown -R nextjs:nodejs /app/.next/cache /app/uploads && \ - chown nextjs:nodejs /app/detail-generator-private && \ - chmod 0700 /app/detail-generator-private && \ - chmod 0750 /app/uploads /app/uploads/temp /app/uploads/assembled /app/uploads/materials /app/uploads/cleanup && \ +# 代码在 COPY 时直接归 root,避免再次改动整棵目录产生重复镜像层。 +# 仅运行时缓存和存储目录允许 nextjs 写入,代码权限保持只读。 +RUN install -d -o nextjs -g nodejs -m 0755 /app/.next/cache && \ + install -d -o nextjs -g nodejs -m 0750 /app/uploads /app/uploads/temp /app/uploads/assembled /app/uploads/materials /app/uploads/cleanup && \ + install -d -o nextjs -g nodejs -m 0700 /app/detail-generator-private && \ test "$(cat /app/DEPLOYMENT_COMMIT)" = "$VCS_REF" && \ - chmod 0444 /app/DEPLOYMENT_COMMIT && \ + test "$(stat -c '%a' /app/DEPLOYMENT_COMMIT)" = 444 && \ test -s /app/public/brand/header-logo.png && \ test -s /app/public/brand/login-logo.png && \ test -s /app/public/brand/favicon.png && \ diff --git a/scripts/database-migrator-lock.mjs b/scripts/database-migrator-lock.mjs new file mode 100644 index 0000000..44afc3f --- /dev/null +++ b/scripts/database-migrator-lock.mjs @@ -0,0 +1,101 @@ +import assert from 'node:assert/strict' +import { readFile, writeFile } from 'node:fs/promises' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const MANIFEST = 'workers/database-migrator/package.json' +const LOCK = 'workers/database-migrator/package-lock.json' +const DIRECT_DEPENDENCIES = ['@prisma/client', 'prisma', 'tsx', 'zod'] + +// Preserve the main lock's exact locations, versions and integrity hashes. In +// particular Prisma 5's nested engines must not resolve to the app's Prisma 6 +// engines. npm ci consumes this subset without resolving a new dependency tree. +export function createMigratorLock(rootLock, manifest) { + assert.equal(rootLock.lockfileVersion, 3, 'Main lock must use lockfileVersion 3') + assert.deepEqual(Object.keys(manifest.dependencies).sort(), DIRECT_DEPENDENCIES, + 'Migrator direct dependencies must be explicitly reviewed') + assert.ok(!manifest.devDependencies && !manifest.optionalDependencies, + 'Migrator manifest cannot introduce unreviewed dependency groups') + + const packages = {} + const visit = packagePath => { + if (packages[packagePath]) return + const record = rootLock.packages[packagePath] + assert.ok(record && !record.link, `Missing or linked dependency: ${packagePath}`) + assert.ok(record.version && record.resolved && record.integrity, + `Dependency must have a pinned version, source and integrity: ${packagePath}`) + const copy = structuredClone(record) + // These packages are all runtime dependencies of the one-shot image. + delete copy.dev + delete copy.devOptional + packages[packagePath] = copy + const resolve = name => { + let parent = packagePath + while (parent) { + const candidate = `${parent}/node_modules/${name}` + if (rootLock.packages[candidate]) return candidate + const index = parent.lastIndexOf('/node_modules/') + parent = index < 0 ? '' : parent.slice(0, index) + } + const candidate = `node_modules/${name}` + assert.ok(rootLock.packages[candidate], `Unresolved dependency ${name} from ${packagePath}`) + return candidate + } + for (const name of Object.keys({ ...record.dependencies, ...record.optionalDependencies })) { + visit(resolve(name)) + } + for (const name of Object.keys(record.peerDependencies ?? {})) { + // A newly introduced optional peer could change runtime behavior even + // when npm accepts its absence. Review it instead of silently omitting it. + if (record.peerDependenciesMeta?.[name]?.optional) { + assert.ok(packagePath === 'node_modules/@prisma/client' && name === 'prisma', + `Unreviewed optional peer ${name} from ${packagePath}`) + } + visit(resolve(name)) + } + } + + for (const [name, version] of Object.entries(manifest.dependencies)) { + const packagePath = `node_modules/${name}` + assert.equal(rootLock.packages[packagePath]?.version, version, + `${name} differs from the main lock; review and update the migrator manifest`) + visit(packagePath) + } + return { + name: manifest.name, + version: manifest.version, + lockfileVersion: 3, + requires: true, + packages: { + '': { + name: manifest.name, + version: manifest.version, + dependencies: manifest.dependencies, + engines: manifest.engines, + }, + ...Object.fromEntries(Object.entries(packages).sort(([a], [b]) => a.localeCompare(b, 'en'))), + }, + } +} + +async function main() { + const [mode = '--check', ...extra] = process.argv.slice(2) + assert.ok(['--check', '--write'].includes(mode) && extra.length === 0, + 'Usage: node scripts/database-migrator-lock.mjs [--check|--write]') + const rootLock = JSON.parse(await readFile(path.join(ROOT, 'package-lock.json'), 'utf8')) + const manifest = JSON.parse(await readFile(path.join(ROOT, MANIFEST), 'utf8')) + const expected = createMigratorLock(rootLock, manifest) + if (mode === '--write') { + await writeFile(path.join(ROOT, LOCK), `${JSON.stringify(expected, null, 2)}\n`) + } else { + const actual = JSON.parse(await readFile(path.join(ROOT, LOCK), 'utf8')) + assert.deepEqual(actual, expected, + 'Migrator lock differs from the main lock. Run node scripts/database-migrator-lock.mjs --write and review it.') + } + console.log(`Migrator lock ${mode === '--write' ? 'written' : 'verified'}: ${Object.keys(expected.packages).length - 1} pinned packages`) +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + main().catch(error => { console.error(error.message); process.exitCode = 1 }) +} diff --git a/scripts/database-migrator-lock.test.mjs b/scripts/database-migrator-lock.test.mjs new file mode 100644 index 0000000..b1a1f21 --- /dev/null +++ b/scripts/database-migrator-lock.test.mjs @@ -0,0 +1,112 @@ +import assert from 'node:assert/strict' +import { existsSync, readFileSync } from 'node:fs' +import { createRequire } from 'node:module' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import test from 'node:test' +import { createMigratorLock } from './database-migrator-lock.mjs' + +const read = relative => JSON.parse(readFileSync(fileURLToPath(new URL(relative, import.meta.url)), 'utf8')) +const root = read('../package-lock.json') +const manifest = read('../workers/database-migrator/package.json') +const lock = read('../workers/database-migrator/package-lock.json') + +test('committed migrator lock is the exact reproducible main-lock subset', () => { + assert.deepEqual(lock, createMigratorLock(root, manifest)) + assert.ok(Object.keys(lock.packages).length < 50) + for (const [name, record] of Object.entries(lock.packages)) { + if (!name) continue + for (const key of ['version', 'resolved', 'integrity']) assert.equal(record[key], root.packages[name][key]) + assert.equal(record.dev, undefined) + assert.equal(record.devOptional, undefined) + } +}) + +test('Prisma CLI uses its matching nested engines without app Prisma 6 or UI/media/test packages', () => { + assert.equal(lock.packages['node_modules/prisma/node_modules/@prisma/engines'].version, '5.22.0') + for (const name of ['@prisma/engines', 'next', 'react', 'canvas', 'sharp', 'jest', 'typescript', '@aws-sdk/client-s3']) { + assert.equal(lock.packages[`node_modules/${name}`], undefined, `Unexpected app package ${name}`) + } + assert.ok(lock.packages['node_modules/@esbuild/linux-x64']) + assert.ok(lock.packages['node_modules/get-tsconfig']) +}) + +test('direct dependency version drift and new dependency groups fail closed', () => { + const changedRoot = structuredClone(root) + changedRoot.packages['node_modules/prisma'].version = '6.18.0' + assert.throws(() => createMigratorLock(changedRoot, manifest), /differs from the main lock/) + assert.throws(() => createMigratorLock(root, { ...manifest, devDependencies: { next: '14.2.33' } }), /unreviewed dependency groups/) + assert.throws(() => createMigratorLock(root, { ...manifest, dependencies: { ...manifest.dependencies, next: '14.2.33' } }), /explicitly reviewed/) +}) + +test('missing transitive package and missing integrity fail closed', () => { + const missing = structuredClone(root) + delete missing.packages['node_modules/get-tsconfig'] + assert.throws(() => createMigratorLock(missing, manifest), /Unresolved dependency/) + const unsigned = structuredClone(root) + delete unsigned.packages['node_modules/prisma/node_modules/@prisma/engines'].integrity + assert.throws(() => createMigratorLock(unsigned, manifest), /integrity/) +}) + +test('optional peer additions require explicit review instead of silently disappearing', () => { + const changed = structuredClone(root) + changed.packages['node_modules/zod'].peerDependencies = { next: '*' } + changed.packages['node_modules/zod'].peerDependenciesMeta = { next: { optional: true } } + assert.throws(() => createMigratorLock(changed, manifest), /Unreviewed optional peer/) +}) + +test('slim migrator copies the full seed source closure and no unreviewed runtime dependency', () => { + const require = createRequire(import.meta.url) + const ts = require('typescript') + const rootDir = fileURLToPath(new URL('../', import.meta.url)) + const files = new Set() + const packages = new Set() + const visitFile = relative => { + relative = relative.replaceAll('\\', '/') + if (files.has(relative)) return + files.add(relative) + const absolute = path.join(rootDir, relative) + const ast = ts.createSourceFile(absolute, readFileSync(absolute, 'utf8'), ts.ScriptTarget.Latest, true) + const resolve = specifier => { + if (specifier.startsWith('node:')) return + if (!specifier.startsWith('.') && !specifier.startsWith('@/')) { + packages.add(specifier) + return + } + const base = specifier.startsWith('@/') ? specifier.slice(2) : path.join(path.dirname(relative), specifier) + const next = [base, `${base}.ts`, `${base}.tsx`, `${base}/index.ts`].find(candidate => existsSync(path.join(rootDir, candidate))) + assert.ok(next, `Unresolved seed import ${specifier} from ${relative}`) + visitFile(next) + } + const visitNode = node => { + if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier) resolve(node.moduleSpecifier.text) + if (ts.isCallExpression(node) && node.expression.getText(ast) === 'require' && ts.isStringLiteral(node.arguments[0])) resolve(node.arguments[0].text) + ts.forEachChild(node, visitNode) + } + visitNode(ast) + } + visitFile('scripts/seed-team-function-config.ts') + assert.deepEqual([...packages].sort(), ['@prisma/client', 'zod']) + const dockerfile = readFileSync(path.join(rootDir, 'Dockerfile'), 'utf8') + const migrator = dockerfile.slice(dockerfile.search(/^FROM base AS migrator\r?$/m), dockerfile.search(/^FROM base AS runner\r?$/m)) + for (const file of files) assert.ok(migrator.includes(`/app/${file}`), `Seed dependency missing from slim image: ${file}`) + assert.equal(files.size, 14, 'Review seed dependency closure before changing the image copy list') +}) + +test('Docker locks the isolated install and runs Prisma as nonroot with no network', () => { + const dockerfile = readFileSync(fileURLToPath(new URL('../Dockerfile', import.meta.url)), 'utf8') + assert.match(dockerfile, /node scripts\/database-migrator-lock\.mjs --check/) + const depsStart = dockerfile.search(/^FROM base AS migrator-deps\r?$/m) + assert.ok(depsStart >= 0) + const deps = dockerfile.slice(depsStart, dockerfile.indexOf('\nFROM ', depsStart + 1)) + assert.match(deps, /npm ci --no-audit --no-fund/) + assert.match(deps, /PRISMA_SKIP_POSTINSTALL_GENERATE=1 npm ci/) + assert.doesNotMatch(deps, /--ignore-scripts/) + assert.doesNotMatch(deps, /npm (?:install|update)/) + assert.match(deps, /apk add --no-cache openssl/) + const start = dockerfile.search(/^FROM base AS migrator\r?$/m) + const migrator = dockerfile.slice(start, dockerfile.indexOf('\nFROM ', start + 1)) + assert.match(migrator, /USER node[\s\S]*RUN --network=none[^\n]*prisma (?:--version|-v)/) + assert.match(migrator, /COPY --from=builder --chown=root:root \/app\/node_modules\/\.prisma/) + assert.match(migrator, /ENTRYPOINT \["node", "scripts\/safe-production-migrate\.mjs"\]/) +}) diff --git a/scripts/safe-production-migrate.test.mjs b/scripts/safe-production-migrate.test.mjs index 80deb50..dc754d3 100644 --- a/scripts/safe-production-migrate.test.mjs +++ b/scripts/safe-production-migrate.test.mjs @@ -891,7 +891,7 @@ test('container supply chain and migrator image are immutable while SOP keeps co readFile(new URL('../.dockerignore', import.meta.url), 'utf8'), readFile(new URL('./git-incremental-deploy.ps1', import.meta.url), 'utf8'), ]) - const migratorStart = dockerfile.indexOf('FROM base AS migrator') + const migratorStart = dockerfile.search(/^FROM base AS migrator\r?$/m) const runnerStart = dockerfile.indexOf('FROM base AS runner', migratorStart) const migrator = dockerfile.slice(migratorStart, runnerStart) const depsStart = dockerfile.indexOf('FROM base AS deps') @@ -916,10 +916,12 @@ test('container supply chain and migrator image are immutable while SOP keeps co assert.match(migrator, /command -v pg_dump >\/dev\/null/) assert.match(migrator, /command -v pg_restore >\/dev\/null/) assert.match(migrator, /command -v psql >\/dev\/null/) - assert.match(migrator, /COPY --from=builder \/app\/scripts\/create-verified-production-backup\.mjs/) - assert.match(migrator, /test -x \/app\/node_modules\/prisma\/node_modules\/@prisma\/engines\/schema-engine-linux-musl/) - assert.match(migrator, /chown -R node:node \/app\/node_modules\/prisma\/node_modules\/@prisma\/engines/) - assert.match(migrator, /chmod -R a\+rX,a-w \/app\/prisma \/app\/scripts \/app\/DEPLOYMENT_COMMIT/) + assert.match(migrator, /COPY --from=builder --chown=root:root \/app\/scripts\/create-verified-production-backup\.mjs/) + assert.match(migrator, /test -x \/app\/node_modules\/prisma\/node_modules\/@prisma\/engines\/schema-engine-linux-musl-openssl-3\.0\.x(?:\s|$)/) + assert.doesNotMatch(migrator, /chown\s+(?:-R\s+)?node:node/) + assert.match(migrator, /COPY --from=migrator-deps --chown=root:root \/app\/workers\/database-migrator\/node_modules/) + assert.doesNotMatch(migrator, /COPY[^\n]*\/app\/node_modules\s+\.\/node_modules/) + assert.doesNotMatch(migrator, /COPY[^\n]*\/app\/lib\s+\.\/lib/) assert.match(migrator, /USER node/) assert.match(sop, /tar -xOf "\$MIGRATOR_ARCHIVE" manifest\.json/) assert.match(sop, /entry\.RepoTags\.includes\(image\)/) diff --git a/workers/database-migrator/README.md b/workers/database-migrator/README.md new file mode 100644 index 0000000..ac1cb2e --- /dev/null +++ b/workers/database-migrator/README.md @@ -0,0 +1,26 @@ +# Database migrator dependencies + +此目录只为一次性数据库工具提供依赖,不安装完整应用依赖,也不改变生产迁移、备份和恢复的授权门禁。 + +| 依赖 | 用途 | +| --- | --- | +| `@prisma/client` | 迁移前后的数据库结构、身份核验;团队职能 seed 查询与写入 | +| `prisma` | 执行 `prisma migrate deploy` 及所需的原生 schema engine | +| `tsx` | 执行现有 TypeScript 团队职能 seed,不复制 TypeScript 编译器 | +| `zod` | 复用团队职能输入校验 | + +独立 lock 从根 `package-lock.json` 提取完整依赖闭包,保留精确版本、下载来源、integrity 和嵌套路径。Prisma CLI 使用自身锁定的 5.22 引擎,不使用应用根目录的另一版本引擎。生成器会检查直接依赖版本;新增可选 peer 也必须先人工核对,不能静默省略。 + +根 lock 更新后,先检查这四个依赖的实际锁定版本是否变化;有变化时审核兼容性并同步本目录 `package.json`,然后执行: + +```sh +node scripts/database-migrator-lock.mjs --write +node scripts/database-migrator-lock.mjs --check +node --test scripts/database-migrator-lock.test.mjs scripts/safe-production-migrate.test.mjs +``` + +审核独立 lock 的差异后重新构建 `migrator`。构建过程先执行 `--check`,根 lock 与独立 lock 不一致时直接失败,不重新解析版本。 + +Docker 的独立依赖阶段先安装 OpenSSL 3,再执行 `PRISMA_SKIP_POSTINSTALL_GENERATE=1 npm ci`,将匹配 Alpine 的 `schema-engine-linux-musl-openssl-3.0.x` 打包。该变量仅跳过此无 schema 目录中多余的 Client 自动生成;`@prisma/engines` 的安装脚本照常执行。生成的 Prisma Client 从同次应用 builder 的 `.prisma` 目录复制,保证与相同版本的 schema 一致。最终镜像以 `node` 用户运行,代码和引擎归 root 所有,并在断网条件下执行 Prisma CLI 验证,避免运行时下载引擎。 + +团队职能 seed 仍复用项目原有权限、花名册及数据写入逻辑。镜像只复制其 13 个 `lib` 源文件;测试解析脚本的完整导入关系,检查复制列表及外部依赖。修改 seed 的导入关系后,必须重新审核这一依赖闭包。备份工具继续依赖系统提供的 `pg_dump`、`pg_restore` 和 `psql`。 diff --git a/workers/database-migrator/package-lock.json b/workers/database-migrator/package-lock.json new file mode 100644 index 0000000..cc538da --- /dev/null +++ b/workers/database-migrator/package-lock.json @@ -0,0 +1,623 @@ +{ + "name": "gongxiangp2-database-migrator", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gongxiangp2-database-migrator", + "version": "1.0.0", + "dependencies": { + "@prisma/client": "5.22.0", + "prisma": "5.22.0", + "tsx": "4.21.0", + "zod": "3.25.76" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@prisma/client": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", + "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.12.0.tgz", + "integrity": "sha512-LScr2aNr2FbjAjZh2C6X6BxRx1/x+aTDExct/xyq2XKbYOiG5c0aK7pMsSuyc0brz3ibr/lbQiHD9jzt4lccJw==", + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/prisma": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/engines": "5.22.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + } + }, + "node_modules/prisma/node_modules/@prisma/debug": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "license": "Apache-2.0" + }, + "node_modules/prisma/node_modules/@prisma/engines": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", + "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/fetch-engine": "5.22.0", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/prisma/node_modules/@prisma/engines-version": { + "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", + "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "license": "Apache-2.0" + }, + "node_modules/prisma/node_modules/@prisma/fetch-engine": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", + "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/prisma/node_modules/@prisma/get-platform": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", + "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/workers/database-migrator/package.json b/workers/database-migrator/package.json new file mode 100644 index 0000000..9581dba --- /dev/null +++ b/workers/database-migrator/package.json @@ -0,0 +1,15 @@ +{ + "name": "gongxiangp2-database-migrator", + "version": "1.0.0", + "private": true, + "description": "Isolated dependencies for migration, verified backup and team-function seed tools", + "engines": { + "node": ">=18" + }, + "dependencies": { + "@prisma/client": "5.22.0", + "prisma": "5.22.0", + "tsx": "4.21.0", + "zod": "3.25.76" + } +}