diff --git a/package.json b/package.json index bdb82630f..4f9231827 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "build:db-mongodb": "turbo build --filter db-mongodb", "build:db-postgres": "turbo build --filter db-postgres", "build:db-sqlite": "turbo build --filter db-sqlite", + "build:db-vercel-postgres": "turbo build --filter db-vercel-postgres", "build:drizzle": "turbo build --filter drizzle", "build:email-nodemailer": "turbo build --filter email-nodemailer", "build:email-resend": "turbo build --filter email-resend", @@ -58,6 +59,7 @@ "dev:generate-importmap": "pnpm runts ./test/generateImportMap.ts", "dev:generate-types": "pnpm runts ./test/generateTypes.ts", "dev:postgres": "cross-env PAYLOAD_DATABASE=postgres pnpm runts ./test/dev.ts", + "dev:vercel-postgres": "cross-env PAYLOAD_DATABASE=vercel-postgres pnpm runts ./test/dev.ts", "devsafe": "node ./scripts/delete-recursively.js '**/.next' && pnpm dev", "docker:restart": "pnpm docker:stop --remove-orphans && pnpm docker:start", "docker:start": "docker compose -f packages/plugin-cloud-storage/docker-compose.yml up -d", diff --git a/packages/db-mongodb/src/index.ts b/packages/db-mongodb/src/index.ts index 9f96aa5ed..0b304183b 100644 --- a/packages/db-mongodb/src/index.ts +++ b/packages/db-mongodb/src/index.ts @@ -182,6 +182,7 @@ export function mongooseAdapter({ init, migrateFresh, migrationDir, + packageName: '@payloadcms/db-mongodb', payload, prodMigrations, queryDrafts, diff --git a/packages/db-postgres/src/index.ts b/packages/db-postgres/src/index.ts index 344870c4d..e44f578a2 100644 --- a/packages/db-postgres/src/index.ts +++ b/packages/db-postgres/src/index.ts @@ -139,6 +139,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj migrateReset, migrateStatus, migrationDir, + packageName: '@payloadcms/db-postgres', payload, queryDrafts, rejectInitializing, diff --git a/packages/db-sqlite/src/index.ts b/packages/db-sqlite/src/index.ts index a33757e09..8cd8ec539 100644 --- a/packages/db-sqlite/src/index.ts +++ b/packages/db-sqlite/src/index.ts @@ -140,6 +140,7 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj { migrateReset, migrateStatus, migrationDir, + packageName: '@payloadcms/db-sqlite', payload, queryDrafts, rejectInitializing, diff --git a/packages/db-vercel-postgres/.gitignore b/packages/db-vercel-postgres/.gitignore new file mode 100644 index 000000000..fc9908001 --- /dev/null +++ b/packages/db-vercel-postgres/.gitignore @@ -0,0 +1 @@ +/migrations diff --git a/packages/db-vercel-postgres/.prettierignore b/packages/db-vercel-postgres/.prettierignore new file mode 100644 index 000000000..247f3f12d --- /dev/null +++ b/packages/db-vercel-postgres/.prettierignore @@ -0,0 +1,10 @@ +.tmp +**/.git +**/.hg +**/.pnp.* +**/.svn +**/.yarn/** +**/build +**/dist/** +**/node_modules +**/temp diff --git a/packages/db-vercel-postgres/.swcrc b/packages/db-vercel-postgres/.swcrc new file mode 100644 index 000000000..14463f4b0 --- /dev/null +++ b/packages/db-vercel-postgres/.swcrc @@ -0,0 +1,15 @@ +{ + "$schema": "https://json.schemastore.org/swcrc", + "sourceMaps": true, + "jsc": { + "target": "esnext", + "parser": { + "syntax": "typescript", + "tsx": true, + "dts": true + } + }, + "module": { + "type": "es6" + } +} diff --git a/packages/db-vercel-postgres/README.md b/packages/db-vercel-postgres/README.md new file mode 100644 index 000000000..ed25dac58 --- /dev/null +++ b/packages/db-vercel-postgres/README.md @@ -0,0 +1,43 @@ +# Payload Postgres Adapter + +[Vercel Postgres](https://vercel.com/docs/storage/vercel-postgres) adapter for [Payload](https://payloadcms.com). + +- [Main Repository](https://github.com/payloadcms/payload) +- [Payload Docs](https://payloadcms.com/docs) + +## Installation + +```bash +npm install @payloadcms/db-vercel-postgres +``` + +## Usage + +### Explicit Connection String + +```ts +import { buildConfig } from 'payload' +import { vercelPostgresAdapter } from '@payloadcms/db-vercel-postgres' + +export default buildConfig({ + db: vercelPostgresAdapter({ + pool: { + connectionString: process.env.DATABASE_URI, + }, + }), + // ...rest of config +}) +``` + +### Automatic Connection String Detection + +Have Vercel automatically detect from environment variable (typically `process.env.POSTGRES_URL`) + +```ts +export default buildConfig({ + db: postgresAdapter(), + // ...rest of config +}) +``` + +More detailed usage can be found in the [Payload Docs](https://payloadcms.com/docs/configuration/overview). diff --git a/packages/db-vercel-postgres/eslint.config.js b/packages/db-vercel-postgres/eslint.config.js new file mode 100644 index 000000000..e62b92c80 --- /dev/null +++ b/packages/db-vercel-postgres/eslint.config.js @@ -0,0 +1,20 @@ +import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js' + +/** @typedef {import('eslint').Linter.FlatConfig} */ +let FlatConfig + +/** @type {FlatConfig[]} */ +export const index = [ + ...rootEslintConfig, + { + languageOptions: { + parserOptions: { + project: './tsconfig.json', + tsconfigDirName: import.meta.dirname, + ...rootParserOptions, + }, + }, + }, +] + +export default index diff --git a/packages/db-vercel-postgres/package.json b/packages/db-vercel-postgres/package.json new file mode 100644 index 000000000..d5298ce9c --- /dev/null +++ b/packages/db-vercel-postgres/package.json @@ -0,0 +1,89 @@ +{ + "name": "@payloadcms/db-vercel-postgres", + "version": "3.0.0-beta.84", + "description": "Vercel Postgres adapter for Payload", + "homepage": "https://payloadcms.com", + "repository": { + "type": "git", + "url": "https://github.com/payloadcms/payload.git", + "directory": "packages/db-vercel-postgres" + }, + "license": "MIT", + "author": "Payload (https://payloadcms.com)", + "type": "module", + "exports": { + ".": { + "import": "./src/index.ts", + "types": "./src/index.ts", + "default": "./src/index.ts" + }, + "./types": { + "import": "./src/types.ts", + "types": "./src/types.ts", + "default": "./src/types.ts" + }, + "./migration-utils": { + "import": "./src/exports/migration-utils.ts", + "types": "./src/exports/migration-utils.ts", + "default": "./src/exports/migration-utils.ts" + } + }, + "main": "./src/index.ts", + "types": "./src/types.ts", + "files": [ + "dist", + "mock.js" + ], + "scripts": { + "build": "rimraf .dist && rimraf tsconfig.tsbuildinfo && pnpm build:types && pnpm build:swc && pnpm build:esbuild && pnpm renamePredefinedMigrations", + "build:esbuild": "echo skipping esbuild", + "build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths", + "build:types": "tsc --emitDeclarationOnly --outDir dist", + "clean": "rimraf {dist,*.tsbuildinfo}", + "prepack": "pnpm clean && pnpm turbo build", + "prepublishOnly": "pnpm clean && pnpm turbo build", + "renamePredefinedMigrations": "node --no-deprecation --import @swc-node/register/esm-register ./scripts/renamePredefinedMigrations.ts" + }, + "dependencies": { + "@payloadcms/drizzle": "workspace:*", + "@vercel/postgres": "^0.9.0", + "console-table-printer": "2.11.2", + "drizzle-kit": "0.23.2-df9e596", + "drizzle-orm": "0.32.1", + "prompts": "2.4.2", + "to-snake-case": "1.0.0", + "uuid": "10.0.0" + }, + "devDependencies": { + "@hyrious/esbuild-plugin-commonjs": "^0.2.4", + "@payloadcms/eslint-config": "workspace:*", + "@types/pg": "8.10.2", + "@types/to-snake-case": "1.0.0", + "esbuild": "0.23.0", + "payload": "workspace:*" + }, + "peerDependencies": { + "payload": "workspace:*" + }, + "publishConfig": { + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./types": { + "import": "./dist/types.js", + "types": "./dist/types.d.ts", + "default": "./dist/types.js" + }, + "./migration-utils": { + "import": "./dist/exports/migration-utils.js", + "types": "./dist/exports/migration-utils.d.ts", + "default": "./dist/exports/migration-utils.js" + } + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts" + } +} diff --git a/packages/db-vercel-postgres/relationships-v2-v3.mjs b/packages/db-vercel-postgres/relationships-v2-v3.mjs new file mode 100644 index 000000000..4f946ea81 --- /dev/null +++ b/packages/db-vercel-postgres/relationships-v2-v3.mjs @@ -0,0 +1,13 @@ +const imports = `import { migratePostgresV2toV3 } from '@payloadcms/migratePostgresV2toV3'` +const up = ` await migratePostgresV2toV3({ + // enables logging of changes that will be made to the database + debug: false, + // skips calls that modify schema or data + dryRun: false, + payload, + req, + }) +` +export { imports, up } + +//# sourceMappingURL=relationships-v2-v3.js.map diff --git a/packages/db-vercel-postgres/scripts/renamePredefinedMigrations.ts b/packages/db-vercel-postgres/scripts/renamePredefinedMigrations.ts new file mode 100644 index 000000000..625f82cf0 --- /dev/null +++ b/packages/db-vercel-postgres/scripts/renamePredefinedMigrations.ts @@ -0,0 +1,19 @@ +import fs from 'fs' +import path from 'path' + +/** + * Changes built .js files to .mjs to for ESM imports + */ +const rename = () => { + fs.readdirSync(path.resolve('./dist/predefinedMigrations')) + .filter((f) => { + return f.endsWith('.js') + }) + .forEach((file) => { + const newPath = path.join('./dist/predefinedMigrations', file) + fs.renameSync(newPath, newPath.replace('.js', '.mjs')) + }) + console.log('done') +} + +rename() diff --git a/packages/db-vercel-postgres/src/connect.ts b/packages/db-vercel-postgres/src/connect.ts new file mode 100644 index 000000000..f90f4e745 --- /dev/null +++ b/packages/db-vercel-postgres/src/connect.ts @@ -0,0 +1,61 @@ +import type { DrizzleAdapter } from '@payloadcms/drizzle/types' +import type { Connect } from 'payload' + +import { pushDevSchema } from '@payloadcms/drizzle' +import { VercelPool, sql } from '@vercel/postgres' +import { drizzle } from 'drizzle-orm/node-postgres' + +import type { VercelPostgresAdapter } from './types.js' + +export const connect: Connect = async function connect( + this: VercelPostgresAdapter, + options = { + hotReload: false, + }, +) { + const { hotReload } = options + + this.schema = { + pgSchema: this.pgSchema, + ...this.tables, + ...this.relations, + ...this.enums, + } + + try { + const logger = this.logger || false + // Passed the poolOptions if provided, + // else have vercel/postgres detect the connection string from the environment + this.drizzle = drizzle(this.poolOptions ? new VercelPool(this.poolOptions) : sql, { + logger, + schema: this.schema, + }) + + if (!hotReload) { + if (process.env.PAYLOAD_DROP_DATABASE === 'true') { + this.payload.logger.info(`---- DROPPING TABLES SCHEMA(${this.schemaName || 'public'}) ----`) + await this.dropDatabase({ adapter: this }) + this.payload.logger.info('---- DROPPED TABLES ----') + } + } + } catch (err) { + this.payload.logger.error(`Error: cannot connect to Postgres. Details: ${err.message}`, err) + if (typeof this.rejectInitializing === 'function') this.rejectInitializing() + process.exit(1) + } + + // Only push schema if not in production + if ( + process.env.NODE_ENV !== 'production' && + process.env.PAYLOAD_MIGRATING !== 'true' && + this.push !== false + ) { + await pushDevSchema(this as unknown as DrizzleAdapter) + } + + if (typeof this.resolveInitializing === 'function') this.resolveInitializing() + + if (process.env.NODE_ENV === 'production' && this.prodMigrations) { + await this.migrate({ migrations: this.prodMigrations }) + } +} diff --git a/packages/db-vercel-postgres/src/exports/migration-utils.ts b/packages/db-vercel-postgres/src/exports/migration-utils.ts new file mode 100644 index 000000000..e67c9579f --- /dev/null +++ b/packages/db-vercel-postgres/src/exports/migration-utils.ts @@ -0,0 +1 @@ +export { migratePostgresV2toV3 } from '../predefinedMigrations/v2-v3/index.js' diff --git a/packages/db-vercel-postgres/src/index.ts b/packages/db-vercel-postgres/src/index.ts new file mode 100644 index 000000000..8ab7fd0cd --- /dev/null +++ b/packages/db-vercel-postgres/src/index.ts @@ -0,0 +1,163 @@ +import type { DatabaseAdapterObj, Payload } from 'payload' + +import { + beginTransaction, + commitTransaction, + count, + create, + createGlobal, + createGlobalVersion, + createVersion, + deleteMany, + deleteOne, + deleteVersions, + destroy, + find, + findGlobal, + findGlobalVersions, + findMigrationDir, + findOne, + findVersions, + migrate, + migrateDown, + migrateFresh, + migrateRefresh, + migrateReset, + migrateStatus, + operatorMap, + queryDrafts, + rollbackTransaction, + updateGlobal, + updateGlobalVersion, + updateOne, + updateVersion, +} from '@payloadcms/drizzle' +import { + convertPathToJSONTraversal, + countDistinct, + createJSONQuery, + createMigration, + defaultDrizzleSnapshot, + deleteWhere, + dropDatabase, + execute, + getMigrationTemplate, + init, + insert, + requireDrizzleKit, +} from '@payloadcms/drizzle/postgres' +import { pgEnum, pgSchema, pgTable } from 'drizzle-orm/pg-core' +import { createDatabaseAdapter } from 'payload' + +import type { Args, VercelPostgresAdapter } from './types.js' + +import { connect } from './connect.js' + +export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj { + const postgresIDType = args.idType || 'serial' + const payloadIDType = postgresIDType === 'serial' ? 'number' : 'text' + + function adapter({ payload }: { payload: Payload }) { + const migrationDir = findMigrationDir(args.migrationDir) + let resolveInitializing + let rejectInitializing + let adapterSchema: VercelPostgresAdapter['pgSchema'] + + const initializing = new Promise((res, rej) => { + resolveInitializing = res + rejectInitializing = rej + }) + + if (args.schemaName) { + adapterSchema = pgSchema(args.schemaName) + } else { + adapterSchema = { enum: pgEnum, table: pgTable } + } + + return createDatabaseAdapter({ + name: 'postgres', + defaultDrizzleSnapshot, + drizzle: undefined, + enums: {}, + features: { + json: true, + }, + fieldConstraints: {}, + getMigrationTemplate, + idType: postgresIDType, + initializing, + localesSuffix: args.localesSuffix || '_locales', + logger: args.logger, + operators: operatorMap, + pgSchema: adapterSchema, + pool: undefined, + poolOptions: args.pool, + prodMigrations: args.prodMigrations, + push: args.push, + relations: {}, + relationshipsSuffix: args.relationshipsSuffix || '_rels', + schema: {}, + schemaName: args.schemaName, + sessions: {}, + tableNameMap: new Map(), + tables: {}, + transactionOptions: args.transactionOptions || undefined, + versionsSuffix: args.versionsSuffix || '_v', + + // DatabaseAdapter + beginTransaction: args.transactionOptions === false ? undefined : beginTransaction, + commitTransaction, + connect, + convertPathToJSONTraversal, + count, + countDistinct, + create, + createGlobal, + createGlobalVersion, + createJSONQuery, + createMigration, + createVersion, + defaultIDType: payloadIDType, + deleteMany, + deleteOne, + deleteVersions, + deleteWhere, + destroy, + dropDatabase, + execute, + find, + findGlobal, + findGlobalVersions, + findOne, + findVersions, + init, + insert, + migrate, + migrateDown, + migrateFresh, + migrateRefresh, + migrateReset, + migrateStatus, + migrationDir, + packageName: '@payloadcms/db-vercel-postgres', + payload, + queryDrafts, + rejectInitializing, + requireDrizzleKit, + resolveInitializing, + rollbackTransaction, + updateGlobal, + updateGlobalVersion, + updateOne, + updateVersion, + }) + } + + return { + defaultIDType: payloadIDType, + init: adapter, + } +} + +export type { MigrateDownArgs, MigrateUpArgs } from '@payloadcms/drizzle/postgres' +export { sql } from 'drizzle-orm' diff --git a/packages/db-vercel-postgres/src/predefinedMigrations/relationships-v2-v3.ts b/packages/db-vercel-postgres/src/predefinedMigrations/relationships-v2-v3.ts new file mode 100644 index 000000000..a7903fbc8 --- /dev/null +++ b/packages/db-vercel-postgres/src/predefinedMigrations/relationships-v2-v3.ts @@ -0,0 +1,10 @@ +const imports = `import { migratePostgresV2toV3 } from '@payloadcms/db-postgres/migration-utils'` +const upSQL = ` await migratePostgresV2toV3({ + // enables logging of changes that will be made to the database + debug: false, + payload, + req, + }) +` + +export { imports, upSQL } diff --git a/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/fetchAndResave/index.ts b/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/fetchAndResave/index.ts new file mode 100644 index 000000000..9a72f2d6b --- /dev/null +++ b/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/fetchAndResave/index.ts @@ -0,0 +1,237 @@ +import type { TransactionPg } from '@payloadcms/drizzle/types' +import type { Field, Payload, PayloadRequest } from 'payload' + +import { upsertRow } from '@payloadcms/drizzle' + +import type { VercelPostgresAdapter } from '../../../types.js' +import type { DocsToResave } from '../types.js' + +import { traverseFields } from './traverseFields.js' + +type Args = { + adapter: VercelPostgresAdapter + collectionSlug?: string + db: TransactionPg + debug: boolean + docsToResave: DocsToResave + fields: Field[] + globalSlug?: string + isVersions: boolean + payload: Payload + req: PayloadRequest + tableName: string +} + +export const fetchAndResave = async ({ + adapter, + collectionSlug, + db, + debug, + docsToResave, + fields, + globalSlug, + isVersions, + payload, + req, + tableName, +}: Args) => { + for (const [id, rows] of Object.entries(docsToResave)) { + if (collectionSlug) { + const collectionConfig = payload.collections[collectionSlug].config + + if (collectionConfig) { + if (isVersions) { + const doc = await payload.findVersionByID({ + id, + collection: collectionSlug, + depth: 0, + fallbackLocale: null, + locale: 'all', + req, + showHiddenFields: true, + }) + + if (debug) { + payload.logger.info( + `The collection "${collectionConfig.slug}" version with ID ${id} will be migrated`, + ) + } + + traverseFields({ + doc, + fields, + path: '', + rows, + }) + + try { + await upsertRow({ + id: doc.id, + adapter, + data: doc, + db, + fields, + ignoreResult: true, + operation: 'update', + req, + tableName, + }) + } catch (err) { + payload.logger.error( + `"${collectionConfig.slug}" version with ID ${doc.id} FAILED TO MIGRATE`, + ) + + throw err + } + + if (debug) { + payload.logger.info( + `"${collectionConfig.slug}" version with ID ${doc.id} migrated successfully!`, + ) + } + } else { + const doc = await payload.findByID({ + id, + collection: collectionSlug, + depth: 0, + fallbackLocale: null, + locale: 'all', + req, + showHiddenFields: true, + }) + + if (debug) { + payload.logger.info( + `The collection "${collectionConfig.slug}" with ID ${doc.id} will be migrated`, + ) + } + + traverseFields({ + doc, + fields, + path: '', + rows, + }) + + try { + await upsertRow({ + id: doc.id, + adapter, + data: doc, + db, + fields, + ignoreResult: true, + operation: 'update', + req, + tableName, + }) + } catch (err) { + payload.logger.error( + `The collection "${collectionConfig.slug}" with ID ${doc.id} has FAILED TO MIGRATE`, + ) + + throw err + } + + if (debug) { + payload.logger.info( + `The collection "${collectionConfig.slug}" with ID ${doc.id} has migrated successfully!`, + ) + } + } + } + } + + if (globalSlug) { + const globalConfig = payload.config.globals?.find((global) => global.slug === globalSlug) + + if (globalConfig) { + if (isVersions) { + const { docs } = await payload.findGlobalVersions({ + slug: globalSlug, + depth: 0, + fallbackLocale: null, + limit: 0, + locale: 'all', + req, + showHiddenFields: true, + }) + + if (debug) { + payload.logger.info(`${docs.length} global "${globalSlug}" versions will be migrated`) + } + + for (const doc of docs) { + traverseFields({ + doc, + fields, + path: '', + rows, + }) + + try { + await upsertRow({ + id: doc.id, + adapter, + data: doc, + db, + fields, + ignoreResult: true, + operation: 'update', + req, + tableName, + }) + } catch (err) { + payload.logger.error(`"${globalSlug}" version with ID ${doc.id} FAILED TO MIGRATE`) + + throw err + } + + if (debug) { + payload.logger.info( + `"${globalSlug}" version with ID ${doc.id} migrated successfully!`, + ) + } + } + } else { + const doc = await payload.findGlobal({ + slug: globalSlug, + depth: 0, + fallbackLocale: null, + locale: 'all', + req, + showHiddenFields: true, + }) + + traverseFields({ + doc, + fields, + path: '', + rows, + }) + + try { + await upsertRow({ + adapter, + data: doc, + db, + fields, + ignoreResult: true, + operation: 'update', + req, + tableName, + }) + } catch (err) { + payload.logger.error(`The global "${globalSlug}" has FAILED TO MIGRATE`) + + throw err + } + + if (debug) { + payload.logger.info(`The global "${globalSlug}" has migrated successfully!`) + } + } + } + } + } +} diff --git a/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/fetchAndResave/traverseFields.ts b/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/fetchAndResave/traverseFields.ts new file mode 100644 index 000000000..768cff23c --- /dev/null +++ b/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/fetchAndResave/traverseFields.ts @@ -0,0 +1,215 @@ +import type { Field } from 'payload' + +import { tabHasName } from 'payload/shared' + +type Args = { + doc: Record + fields: Field[] + locale?: string + path: string + rows: Record[] +} + +export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => { + fields.forEach((field) => { + switch (field.type) { + case 'group': { + const newPath = `${path ? `${path}.` : ''}${field.name}` + const newDoc = doc?.[field.name] + + if (typeof newDoc === 'object' && newDoc !== null) { + if (field.localized) { + Object.entries(newDoc).forEach(([locale, localeDoc]) => { + return traverseFields({ + doc: localeDoc, + fields: field.fields, + locale, + path: newPath, + rows, + }) + }) + } else { + return traverseFields({ + doc: newDoc as Record, + fields: field.fields, + path: newPath, + rows, + }) + } + } + + break + } + + case 'row': + case 'collapsible': { + return traverseFields({ + doc, + fields: field.fields, + path, + rows, + }) + } + + case 'array': { + const rowData = doc?.[field.name] + + if (field.localized && typeof rowData === 'object' && rowData !== null) { + Object.entries(rowData).forEach(([locale, localeRows]) => { + if (Array.isArray(localeRows)) { + localeRows.forEach((row, i) => { + return traverseFields({ + doc: row as Record, + fields: field.fields, + locale, + path: `${path ? `${path}.` : ''}${field.name}.${i}`, + rows, + }) + }) + } + }) + } + + if (Array.isArray(rowData)) { + rowData.forEach((row, i) => { + return traverseFields({ + doc: row as Record, + fields: field.fields, + path: `${path ? `${path}.` : ''}${field.name}.${i}`, + rows, + }) + }) + } + + break + } + + case 'blocks': { + const rowData = doc?.[field.name] + + if (field.localized && typeof rowData === 'object' && rowData !== null) { + Object.entries(rowData).forEach(([locale, localeRows]) => { + if (Array.isArray(localeRows)) { + localeRows.forEach((row, i) => { + const matchedBlock = field.blocks.find((block) => block.slug === row.blockType) + + if (matchedBlock) { + return traverseFields({ + doc: row as Record, + fields: matchedBlock.fields, + locale, + path: `${path ? `${path}.` : ''}${field.name}.${i}`, + rows, + }) + } + }) + } + }) + } + + if (Array.isArray(rowData)) { + rowData.forEach((row, i) => { + const matchedBlock = field.blocks.find((block) => block.slug === row.blockType) + + if (matchedBlock) { + return traverseFields({ + doc: row as Record, + fields: matchedBlock.fields, + path: `${path ? `${path}.` : ''}${field.name}.${i}`, + rows, + }) + } + }) + } + + break + } + + case 'tabs': { + return field.tabs.forEach((tab) => { + if (tabHasName(tab)) { + const newDoc = doc?.[tab.name] + const newPath = `${path ? `${path}.` : ''}${tab.name}` + + if (typeof newDoc === 'object' && newDoc !== null) { + if (tab.localized) { + Object.entries(newDoc).forEach(([locale, localeDoc]) => { + return traverseFields({ + doc: localeDoc, + fields: tab.fields, + locale, + path: newPath, + rows, + }) + }) + } else { + return traverseFields({ + doc: newDoc as Record, + fields: tab.fields, + path: newPath, + rows, + }) + } + } + } else { + traverseFields({ + doc, + fields: tab.fields, + path, + rows, + }) + } + }) + } + + case 'relationship': + case 'upload': { + if (typeof field.relationTo === 'string') { + if (field.type === 'upload' || !field.hasMany) { + const relationshipPath = `${path ? `${path}.` : ''}${field.name}` + + if (field.localized) { + const matchedRelationshipsWithLocales = rows.filter( + (row) => row.path === relationshipPath, + ) + + if (matchedRelationshipsWithLocales.length && !doc[field.name]) { + doc[field.name] = {} + } + + const newDoc = doc[field.name] as Record + + matchedRelationshipsWithLocales.forEach((localeRow) => { + if (typeof localeRow.locale === 'string') { + const [, id] = Object.entries(localeRow).find( + ([key, val]) => + val !== null && !['id', 'locale', 'order', 'parent_id', 'path'].includes(key), + ) + + newDoc[localeRow.locale] = id + } + }) + } else { + const matchedRelationship = rows.find((row) => { + const matchesPath = row.path === relationshipPath + + if (locale) return matchesPath && locale === row.locale + + return row.path === relationshipPath + }) + + if (matchedRelationship) { + const [, id] = Object.entries(matchedRelationship).find( + ([key, val]) => + val !== null && !['id', 'locale', 'order', 'parent_id', 'path'].includes(key), + ) + + doc[field.name] = id + } + } + } + } + } + } + }) +} diff --git a/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/groupUpSQLStatements.ts b/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/groupUpSQLStatements.ts new file mode 100644 index 000000000..f7ec0045d --- /dev/null +++ b/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/groupUpSQLStatements.ts @@ -0,0 +1,74 @@ +export type Groups = + | 'addColumn' + | 'addConstraint' + | 'dropColumn' + | 'dropConstraint' + | 'dropTable' + | 'notNull' + +/** + * Convert an "ADD COLUMN" statement to an "ALTER COLUMN" statement + * example: ALTER TABLE "pages_blocks_my_block" ADD COLUMN "person_id" integer NOT NULL; + * to: ALTER TABLE "pages_blocks_my_block" ALTER COLUMN "person_id" SET NOT NULL; + * @param sql + */ +function convertAddColumnToAlterColumn(sql) { + // Regular expression to match the ADD COLUMN statement with its constraints + const regex = /ALTER TABLE ("[^"]+") ADD COLUMN ("[^"]+") [\w\s]+ NOT NULL;/ + + // Replace the matched part with "ALTER COLUMN ... SET NOT NULL;" + return sql.replace(regex, 'ALTER TABLE $1 ALTER COLUMN $2 SET NOT NULL;') +} + +export const groupUpSQLStatements = (list: string[]): Record => { + const groups = { + addColumn: 'ADD COLUMN', + // example: ALTER TABLE "posts" ADD COLUMN "category_id" integer + + addConstraint: 'ADD CONSTRAINT', + //example: + // DO $$ BEGIN + // ALTER TABLE "pages_blocks_my_block" ADD CONSTRAINT "pages_blocks_my_block_person_id_users_id_fk" FOREIGN KEY ("person_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action; + // EXCEPTION + // WHEN duplicate_object THEN null; + // END $$; + + dropColumn: 'DROP COLUMN', + // example: ALTER TABLE "_posts_v_rels" DROP COLUMN IF EXISTS "posts_id"; + + dropConstraint: 'DROP CONSTRAINT', + // example: ALTER TABLE "_posts_v_rels" DROP CONSTRAINT "_posts_v_rels_posts_fk"; + + dropTable: 'DROP TABLE', + // example: DROP TABLE "pages_rels"; + + notNull: 'NOT NULL', + // example: ALTER TABLE "pages_blocks_my_block" ALTER COLUMN "person_id" SET NOT NULL; + } + + const result = Object.keys(groups).reduce((result, group: Groups) => { + result[group] = [] + return result + }, {}) as Record + + for (const line of list) { + Object.entries(groups).some(([key, value]) => { + if (line.endsWith('NOT NULL;')) { + // split up the ADD COLUMN and ALTER COLUMN NOT NULL statements + // example: ALTER TABLE "pages_blocks_my_block" ADD COLUMN "person_id" integer NOT NULL; + // becomes two separate statements: + // 1. ALTER TABLE "pages_blocks_my_block" ADD COLUMN "person_id" integer; + // 2. ALTER TABLE "pages_blocks_my_block" ALTER COLUMN "person_id" SET NOT NULL; + result.addColumn.push(line.replace(' NOT NULL;', ';')) + result.notNull.push(convertAddColumnToAlterColumn(line)) + return true + } + if (line.includes(value)) { + result[key].push(line) + return true + } + }) + } + + return result +} diff --git a/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/index.ts b/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/index.ts new file mode 100644 index 000000000..acca68346 --- /dev/null +++ b/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/index.ts @@ -0,0 +1,278 @@ +import type { TransactionPg } from '@payloadcms/drizzle/types' +import type { DrizzleSnapshotJSON } from 'drizzle-kit/api' +import type { Payload, PayloadRequest } from 'payload' + +import { sql } from 'drizzle-orm' +import fs from 'fs' +import { createRequire } from 'module' +import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload' +import toSnakeCase from 'to-snake-case' + +import type { VercelPostgresAdapter } from '../../types.js' +import type { PathsToQuery } from './types.js' + +import { groupUpSQLStatements } from './groupUpSQLStatements.js' +import { migrateRelationships } from './migrateRelationships.js' +import { traverseFields } from './traverseFields.js' + +const require = createRequire(import.meta.url) + +type Args = { + debug?: boolean + payload: Payload + req?: Partial +} + +/** + * Moves upload and relationship columns from the join table and into the tables while moving data + * This is done in the following order: + * ADD COLUMNs + * -- manipulate data to move relationships to new columns + * ADD CONSTRAINTs + * NOT NULLs + * DROP TABLEs + * DROP CONSTRAINTs + * DROP COLUMNs + * @param debug + * @param payload + * @param req + */ +export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => { + const adapter = payload.db as unknown as VercelPostgresAdapter + const db = adapter.sessions[await req.transactionID].db as TransactionPg + const dir = payload.db.migrationDir + + // get the drizzle migrateUpSQL from drizzle using the last schema + const { generateDrizzleJson, generateMigration } = require('drizzle-kit/api') + const drizzleJsonAfter = generateDrizzleJson(adapter.schema) + + // Get the previous migration snapshot + const previousSnapshot = fs + .readdirSync(dir) + .filter((file) => file.endsWith('.json') && !file.endsWith('relationships_v2_v3.json')) + .sort() + .reverse()?.[0] + + if (!previousSnapshot) { + throw new Error( + `No previous migration schema file found! A prior migration from v2 is required to migrate to v3.`, + ) + } + + const drizzleJsonBefore = JSON.parse( + fs.readFileSync(`${dir}/${previousSnapshot}`, 'utf8'), + ) as DrizzleSnapshotJSON + + const generatedSQL = await generateMigration(drizzleJsonBefore, drizzleJsonAfter) + + if (!generatedSQL.length) { + payload.logger.info(`No schema changes needed.`) + process.exit(0) + } + + const sqlUpStatements = groupUpSQLStatements(generatedSQL) + + const addColumnsStatement = sqlUpStatements.addColumn.join('\n') + + if (debug) { + payload.logger.info('CREATING NEW RELATIONSHIP COLUMNS') + payload.logger.info(addColumnsStatement) + } + + await db.execute(sql.raw(addColumnsStatement)) + + for (const collection of payload.config.collections) { + const tableName = adapter.tableNameMap.get(toSnakeCase(collection.slug)) + const pathsToQuery: PathsToQuery = new Set() + + traverseFields({ + adapter, + collectionSlug: collection.slug, + columnPrefix: '', + db, + disableNotNull: false, + fields: collection.fields, + isVersions: false, + newTableName: tableName, + parentTableName: tableName, + path: '', + pathsToQuery, + payload, + rootTableName: tableName, + }) + + await migrateRelationships({ + adapter, + collectionSlug: collection.slug, + db, + debug, + fields: collection.fields, + isVersions: false, + pathsToQuery, + payload, + req, + tableName, + }) + + if (collection.versions) { + const versionsTableName = adapter.tableNameMap.get( + `_${toSnakeCase(collection.slug)}${adapter.versionsSuffix}`, + ) + const versionFields = buildVersionCollectionFields(collection) + const versionPathsToQuery: PathsToQuery = new Set() + + traverseFields({ + adapter, + collectionSlug: collection.slug, + columnPrefix: '', + db, + disableNotNull: true, + fields: versionFields, + isVersions: true, + newTableName: versionsTableName, + parentTableName: versionsTableName, + path: '', + pathsToQuery: versionPathsToQuery, + payload, + rootTableName: versionsTableName, + }) + + await migrateRelationships({ + adapter, + collectionSlug: collection.slug, + db, + debug, + fields: versionFields, + isVersions: true, + pathsToQuery: versionPathsToQuery, + payload, + req, + tableName: versionsTableName, + }) + } + } + + for (const global of payload.config.globals) { + const tableName = adapter.tableNameMap.get(toSnakeCase(global.slug)) + + const pathsToQuery: PathsToQuery = new Set() + + traverseFields({ + adapter, + columnPrefix: '', + db, + disableNotNull: false, + fields: global.fields, + globalSlug: global.slug, + isVersions: false, + newTableName: tableName, + parentTableName: tableName, + path: '', + pathsToQuery, + payload, + rootTableName: tableName, + }) + + await migrateRelationships({ + adapter, + db, + debug, + fields: global.fields, + globalSlug: global.slug, + isVersions: false, + pathsToQuery, + payload, + req, + tableName, + }) + + if (global.versions) { + const versionsTableName = adapter.tableNameMap.get( + `_${toSnakeCase(global.slug)}${adapter.versionsSuffix}`, + ) + + const versionFields = buildVersionGlobalFields(global) + + const versionPathsToQuery: PathsToQuery = new Set() + + traverseFields({ + adapter, + columnPrefix: '', + db, + disableNotNull: true, + fields: versionFields, + globalSlug: global.slug, + isVersions: true, + newTableName: versionsTableName, + parentTableName: versionsTableName, + path: '', + pathsToQuery: versionPathsToQuery, + payload, + rootTableName: versionsTableName, + }) + + await migrateRelationships({ + adapter, + db, + debug, + fields: versionFields, + globalSlug: global.slug, + isVersions: true, + pathsToQuery: versionPathsToQuery, + payload, + req, + tableName: versionsTableName, + }) + } + } + + // ADD CONSTRAINT + const addConstraintsStatement = sqlUpStatements.addConstraint.join('\n') + + if (debug) { + payload.logger.info('ADDING CONSTRAINTS') + payload.logger.info(addConstraintsStatement) + } + + await db.execute(sql.raw(addConstraintsStatement)) + + // NOT NULL + const notNullStatements = sqlUpStatements.notNull.join('\n') + + if (debug) { + payload.logger.info('NOT NULL CONSTRAINTS') + payload.logger.info(notNullStatements) + } + + await db.execute(sql.raw(notNullStatements)) + + // DROP TABLE + const dropTablesStatement = sqlUpStatements.dropTable.join('\n') + + if (debug) { + payload.logger.info('DROPPING TABLES') + payload.logger.info(dropTablesStatement) + } + + await db.execute(sql.raw(dropTablesStatement)) + + // DROP CONSTRAINT + const dropConstraintsStatement = sqlUpStatements.dropConstraint.join('\n') + + if (debug) { + payload.logger.info('DROPPING CONSTRAINTS') + payload.logger.info(dropConstraintsStatement) + } + + await db.execute(sql.raw(dropConstraintsStatement)) + + // DROP COLUMN + const dropColumnsStatement = sqlUpStatements.dropColumn.join('\n') + + if (debug) { + payload.logger.info('DROPPING COLUMNS') + payload.logger.info(dropColumnsStatement) + } + + await db.execute(sql.raw(dropColumnsStatement)) +} diff --git a/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/migrateRelationships.ts b/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/migrateRelationships.ts new file mode 100644 index 000000000..c88b3dcff --- /dev/null +++ b/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/migrateRelationships.ts @@ -0,0 +1,103 @@ +import type { TransactionPg } from '@payloadcms/drizzle/types' +import type { Field, Payload, PayloadRequest } from 'payload' + +import { sql } from 'drizzle-orm' + +import type { VercelPostgresAdapter } from '../../types.js' +import type { DocsToResave, PathsToQuery } from './types.js' + +import { fetchAndResave } from './fetchAndResave/index.js' + +type Args = { + adapter: VercelPostgresAdapter + collectionSlug?: string + db: TransactionPg + debug: boolean + fields: Field[] + globalSlug?: string + isVersions: boolean + pathsToQuery: PathsToQuery + payload: Payload + req?: Partial + tableName: string +} + +export const migrateRelationships = async ({ + adapter, + collectionSlug, + db, + debug, + fields, + globalSlug, + isVersions, + pathsToQuery, + payload, + req, + tableName, +}: Args) => { + if (pathsToQuery.size === 0) return + + let offset = 0 + + let paginationResult + + const where = Array.from(pathsToQuery).reduce((statement, path, i) => { + return (statement += ` +"${tableName}${adapter.relationshipsSuffix}"."path" LIKE '${path}'${pathsToQuery.size !== i + 1 ? ' OR' : ''} +`) + }, '') + + while (typeof paginationResult === 'undefined' || paginationResult.rows.length > 0) { + const paginationStatement = `SELECT DISTINCT parent_id FROM ${tableName}${adapter.relationshipsSuffix} WHERE + ${where} ORDER BY parent_id LIMIT 500 OFFSET ${offset * 500}; + ` + + paginationResult = await adapter.drizzle.execute(sql.raw(`${paginationStatement}`)) + + if (paginationResult.rows.length === 0) return + + offset += 1 + + const statement = `SELECT * FROM ${tableName}${adapter.relationshipsSuffix} WHERE + (${where}) AND parent_id IN (${paginationResult.rows.map((row) => row.parent_id).join(', ')}); +` + if (debug) { + payload.logger.info('FINDING ROWS TO MIGRATE') + payload.logger.info(statement) + } + + const result = await adapter.drizzle.execute(sql.raw(`${statement}`)) + + const docsToResave: DocsToResave = {} + + result.rows.forEach((row) => { + const parentID = row.parent_id + + if (typeof parentID === 'string' || typeof parentID === 'number') { + if (!docsToResave[parentID]) docsToResave[parentID] = [] + docsToResave[parentID].push(row) + } + }) + + await fetchAndResave({ + adapter, + collectionSlug, + db, + debug, + docsToResave, + fields, + globalSlug, + isVersions, + payload, + req: req as unknown as PayloadRequest, + tableName, + }) + } + + const deleteStatement = `DELETE FROM ${tableName}${adapter.relationshipsSuffix} WHERE ${where}` + if (debug) { + payload.logger.info('DELETING ROWS') + payload.logger.info(deleteStatement) + } + await db.execute(sql.raw(`${deleteStatement}`)) +} diff --git a/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/traverseFields.ts b/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/traverseFields.ts new file mode 100644 index 000000000..1d37595b8 --- /dev/null +++ b/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/traverseFields.ts @@ -0,0 +1,117 @@ +import type { TransactionPg } from '@payloadcms/drizzle/types' +import type { Field, Payload } from 'payload' + +import { tabHasName } from 'payload/shared' +import toSnakeCase from 'to-snake-case' + +import type { VercelPostgresAdapter } from '../../types.js' +import type { PathsToQuery } from './types.js' + +type Args = { + adapter: VercelPostgresAdapter + collectionSlug?: string + columnPrefix: string + db: TransactionPg + disableNotNull: boolean + fields: Field[] + globalSlug?: string + isVersions: boolean + newTableName: string + parentTableName: string + path: string + pathsToQuery: PathsToQuery + payload: Payload + rootTableName: string +} + +export const traverseFields = (args: Args) => { + args.fields.forEach((field) => { + switch (field.type) { + case 'group': { + let newTableName = `${args.newTableName}_${toSnakeCase(field.name)}` + + if (field.localized && args.payload.config.localization) { + newTableName += args.adapter.localesSuffix + } + + return traverseFields({ + ...args, + columnPrefix: `${args.columnPrefix}${toSnakeCase(field.name)}_`, + fields: field.fields, + newTableName, + path: `${args.path ? `${args.path}.` : ''}${field.name}`, + }) + } + + case 'row': + case 'collapsible': { + return traverseFields({ + ...args, + fields: field.fields, + }) + } + + case 'array': { + const newTableName = args.adapter.tableNameMap.get( + `${args.newTableName}_${toSnakeCase(field.name)}`, + ) + + return traverseFields({ + ...args, + columnPrefix: '', + fields: field.fields, + newTableName, + parentTableName: newTableName, + path: `${args.path ? `${args.path}.` : ''}${field.name}.%`, + }) + } + + case 'blocks': { + return field.blocks.forEach((block) => { + const newTableName = args.adapter.tableNameMap.get( + `${args.rootTableName}_blocks_${toSnakeCase(block.slug)}`, + ) + + traverseFields({ + ...args, + columnPrefix: '', + fields: block.fields, + newTableName, + parentTableName: newTableName, + path: `${args.path ? `${args.path}.` : ''}${field.name}.%`, + }) + }) + } + + case 'tabs': { + return field.tabs.forEach((tab) => { + if (tabHasName(tab)) { + args.columnPrefix = `${args.columnPrefix}_${toSnakeCase(tab.name)}_` + args.path = `${args.path ? `${args.path}.` : ''}${tab.name}` + args.newTableName = `${args.newTableName}_${toSnakeCase(tab.name)}` + + if (tab.localized && args.payload.config.localization) { + args.newTableName += args.adapter.localesSuffix + } + } + + traverseFields({ + ...args, + fields: tab.fields, + }) + }) + } + + case 'relationship': + case 'upload': { + if (typeof field.relationTo === 'string') { + if (field.type === 'upload' || !field.hasMany) { + args.pathsToQuery.add(`${args.path ? `${args.path}.` : ''}${field.name}`) + } + } + + return null + } + } + }) +} diff --git a/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/types.ts b/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/types.ts new file mode 100644 index 000000000..8980e64b9 --- /dev/null +++ b/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/types.ts @@ -0,0 +1,9 @@ +/** + * Set of all paths which should be moved + * This will be built up into one WHERE query + */ +export type PathsToQuery = Set + +export type DocsToResave = { + [id: number | string]: Record[] +} diff --git a/packages/db-vercel-postgres/src/types.ts b/packages/db-vercel-postgres/src/types.ts new file mode 100644 index 000000000..237679505 --- /dev/null +++ b/packages/db-vercel-postgres/src/types.ts @@ -0,0 +1,78 @@ +import type { + BasePostgresAdapter, + GenericEnum, + MigrateDownArgs, + MigrateUpArgs, + PostgresDB, +} from '@payloadcms/drizzle/postgres' +import type { DrizzleAdapter } from '@payloadcms/drizzle/types' +import type { VercelPool, VercelPostgresPoolConfig } from '@vercel/postgres' +import type { DrizzleConfig } from 'drizzle-orm' +import type { PgSchema, PgTableFn, PgTransactionConfig } from 'drizzle-orm/pg-core' + +export type Args = { + connectionString?: string + idType?: 'serial' | 'uuid' + localesSuffix?: string + logger?: DrizzleConfig['logger'] + migrationDir?: string + /** + * Optional pool configuration for Vercel Postgres + * If not provided, vercel/postgres will attempt to use the Vercel environment variables + */ + pool?: VercelPostgresPoolConfig + prodMigrations?: { + down: (args: MigrateDownArgs) => Promise + name: string + up: (args: MigrateUpArgs) => Promise + }[] + push?: boolean + relationshipsSuffix?: string + /** + * The schema name to use for the database + * @experimental This only works when there are not other tables or enums of the same name in the database under a different schema. Awaiting fix from Drizzle. + */ + schemaName?: string + transactionOptions?: PgTransactionConfig | false + versionsSuffix?: string +} + +export type VercelPostgresAdapter = { + pool?: VercelPool + poolOptions?: Args['pool'] +} & BasePostgresAdapter + +declare module 'payload' { + export interface DatabaseAdapter + extends Omit, + DrizzleAdapter { + beginTransaction: (options?: PgTransactionConfig) => Promise + drizzle: PostgresDB + enums: Record + /** + * An object keyed on each table, with a key value pair where the constraint name is the key, followed by the dot-notation field name + * Used for returning properly formed errors from unique fields + */ + fieldConstraints: Record> + idType: Args['idType'] + initializing: Promise + localesSuffix?: string + logger: DrizzleConfig['logger'] + pgSchema?: { table: PgTableFn } | PgSchema + pool: VercelPool + poolOptions: Args['pool'] + prodMigrations?: { + down: (args: MigrateDownArgs) => Promise + name: string + up: (args: MigrateUpArgs) => Promise + }[] + push: boolean + rejectInitializing: () => void + relationshipsSuffix?: string + resolveInitializing: () => void + schema: Record + schemaName?: Args['schemaName'] + tableNameMap: Map + versionsSuffix?: string + } +} diff --git a/packages/db-vercel-postgres/tsconfig.json b/packages/db-vercel-postgres/tsconfig.json new file mode 100644 index 000000000..dfbf56180 --- /dev/null +++ b/packages/db-vercel-postgres/tsconfig.json @@ -0,0 +1,38 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "composite": true, + "noEmit": false, + "emitDeclarationOnly": true, + "outDir": "./dist", + "rootDir": "./src", + "skipLibCheck": true, + }, + "exclude": [ + "dist", + "build", + "tests", + "test", + "node_modules", + "eslint.config.js", + "src/**/*.spec.js", + "src/**/*.spec.jsx", + "src/**/*.spec.ts", + "src/**/*.spec.tsx" + ], + "include": [ + "src", + "src/**/*.ts", + ], + "references": [ + { + "path": "../payload" + }, + { + "path": "../translations" + }, + { + "path": "../drizzle" + } + ] +} diff --git a/packages/drizzle/src/postgres/createMigration.ts b/packages/drizzle/src/postgres/createMigration.ts index 299b44721..143322652 100644 --- a/packages/drizzle/src/postgres/createMigration.ts +++ b/packages/drizzle/src/postgres/createMigration.ts @@ -1,4 +1,3 @@ -import type { DrizzleSnapshotJSON } from 'drizzle-kit/api' import type { CreateMigration } from 'payload' import fs from 'fs' @@ -112,6 +111,7 @@ export const createMigration: CreateMigration = async function createMigration( getMigrationTemplate({ downSQL: downSQL || ` // Migration code`, imports, + packageName: payload.db.packageName, upSQL: upSQL || ` // Migration code`, }), ) diff --git a/packages/drizzle/src/postgres/getMigrationTemplate.ts b/packages/drizzle/src/postgres/getMigrationTemplate.ts index b42b13c75..1f2c8ba9a 100644 --- a/packages/drizzle/src/postgres/getMigrationTemplate.ts +++ b/packages/drizzle/src/postgres/getMigrationTemplate.ts @@ -9,8 +9,9 @@ export const indent = (text: string) => export const getMigrationTemplate = ({ downSQL, imports, + packageName, upSQL, -}: MigrationTemplateArgs): string => `import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres' +}: MigrationTemplateArgs): string => `import { MigrateUpArgs, MigrateDownArgs, sql } from '${packageName}' ${imports ? `${imports}\n` : ''} export async function up({ payload, req }: MigrateUpArgs): Promise { ${indent(upSQL)} diff --git a/packages/drizzle/src/postgres/types.ts b/packages/drizzle/src/postgres/types.ts index 419d21e27..a9070ab2c 100644 --- a/packages/drizzle/src/postgres/types.ts +++ b/packages/drizzle/src/postgres/types.ts @@ -111,8 +111,6 @@ export type BasePostgresAdapter = { logger: DrizzleConfig['logger'] operators: Operators pgSchema?: Schema - // pool: Pool - // poolOptions: Args['pool'] prodMigrations?: { down: (args: MigrateDownArgs) => Promise name: string diff --git a/packages/payload/src/database/types.ts b/packages/payload/src/database/types.ts index 3800d8182..dc68cc525 100644 --- a/packages/payload/src/database/types.ts +++ b/packages/payload/src/database/types.ts @@ -100,6 +100,13 @@ export interface BaseDatabaseAdapter { * The name of the database adapter */ name: string + /** + * Full package name of the database adapter + * + * @example @payloadcms/db-postgres + */ + packageName: string + /** * reference to the instance of payload */ @@ -434,5 +441,6 @@ export type DBIdentifierName = export type MigrationTemplateArgs = { downSQL?: string imports?: string + packageName?: string upSQL?: string } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4349a02e7..87e9dd55e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -383,6 +383,52 @@ importers: specifier: workspace:* version: link:../payload + packages/db-vercel-postgres: + dependencies: + '@payloadcms/drizzle': + specifier: workspace:* + version: link:../drizzle + '@vercel/postgres': + specifier: ^0.9.0 + version: 0.9.0 + console-table-printer: + specifier: 2.11.2 + version: 2.11.2 + drizzle-kit: + specifier: 0.23.2-df9e596 + version: 0.23.2-df9e596 + drizzle-orm: + specifier: 0.32.1 + version: 0.32.1(@libsql/client@0.6.2(bufferutil@4.0.8)(utf-8-validate@6.0.4))(@neondatabase/serverless@0.9.4)(@types/pg@8.10.2)(@vercel/postgres@0.9.0)(pg@8.11.3)(react@19.0.0-rc-06d0b89e-20240801)(types-react@19.0.0-rc.0) + prompts: + specifier: 2.4.2 + version: 2.4.2 + to-snake-case: + specifier: 1.0.0 + version: 1.0.0 + uuid: + specifier: 10.0.0 + version: 10.0.0 + devDependencies: + '@hyrious/esbuild-plugin-commonjs': + specifier: ^0.2.4 + version: 0.2.4(cjs-module-lexer@1.3.1)(esbuild@0.23.0) + '@payloadcms/eslint-config': + specifier: workspace:* + version: link:../eslint-config + '@types/pg': + specifier: 8.10.2 + version: 8.10.2 + '@types/to-snake-case': + specifier: 1.0.0 + version: 1.0.0 + esbuild: + specifier: 0.23.0 + version: 0.23.0 + payload: + specifier: workspace:* + version: link:../payload + packages/drizzle: dependencies: console-table-printer: @@ -1600,6 +1646,9 @@ importers: '@payloadcms/db-sqlite': specifier: workspace:* version: link:../packages/db-sqlite + '@payloadcms/db-vercel-postgres': + specifier: workspace:* + version: link:../packages/db-vercel-postgres '@payloadcms/drizzle': specifier: workspace:* version: link:../packages/drizzle @@ -12239,7 +12288,6 @@ snapshots: '@neondatabase/serverless@0.9.4': dependencies: '@types/pg': 8.11.6 - optional: true '@next/bundle-analyzer@15.0.0-canary.104(bufferutil@4.0.8)': dependencies: @@ -13088,7 +13136,6 @@ snapshots: '@types/node': 20.12.5 pg-protocol: 1.6.1 pg-types: 4.0.2 - optional: true '@types/pluralize@0.0.33': {} @@ -13326,7 +13373,6 @@ snapshots: bufferutil: 4.0.8 utf-8-validate: 6.0.4 ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) - optional: true '@vue/compiler-core@3.4.37': dependencies: @@ -13832,7 +13878,6 @@ snapshots: bufferutil@4.0.8: dependencies: node-gyp-build: 4.8.1 - optional: true bundle-name@3.0.0: dependencies: @@ -16940,8 +16985,7 @@ snapshots: fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 - node-gyp-build@4.8.1: - optional: true + node-gyp-build@4.8.1: {} node-int64@0.4.0: {} @@ -18724,7 +18768,6 @@ snapshots: utf-8-validate@6.0.4: dependencies: node-gyp-build: 4.8.1 - optional: true utf8-byte-length@1.0.5: {} diff --git a/scripts/pack-all-to-dest.ts b/scripts/pack-all-to-dest.ts index 912daf9e4..0d30a85c8 100644 --- a/scripts/pack-all-to-dest.ts +++ b/scripts/pack-all-to-dest.ts @@ -9,6 +9,8 @@ import { fileURLToPath } from 'node:url' import path from 'path' import util from 'util' +import type { PackageDetails } from './lib/getPackageDetails.js' + import { getPackageDetails } from './lib/getPackageDetails.js' const execOpts: ExecSyncOptions = { stdio: 'inherit' } @@ -47,6 +49,7 @@ async function main() { 'drizzle', 'db-sqlite', 'db-postgres', + 'db-vercel-postgres', 'richtext-lexical', 'translations', 'plugin-cloud', @@ -58,19 +61,17 @@ async function main() { // Prebuild all packages header(`\n🔨 Prebuilding all packages...`) - //await execa('pnpm', ['install'], execaOpts) - const filtered = packageDetails.filter((p): p is Exclude => p !== null) - header(`\nOutputting ${filtered.length} packages... - -${chalk.white.bold(filtered.map((p) => p.name).join('\n'))} -`) if (!noBuild) { execSync('pnpm build:all --output-logs=errors-only', { stdio: 'inherit' }) } - header(`\n 📦 Packing all packages to ${dest}...`) + header(`\nOutputting ${filtered.length} packages... + +${chalk.white.bold(listPackages(filtered))}`) + + header(`\n📦 Packing all packages to ${dest}...`) await Promise.all( filtered.map(async (p) => { @@ -84,3 +85,7 @@ ${chalk.white.bold(filtered.map((p) => p.name).join('\n'))} function header(message: string, opts?: { enable?: boolean }) { console.log(chalk.bold.green(`${message}\n`)) } + +function listPackages(packages: PackageDetails[]) { + return packages.map((p) => ` - ${p.name}`).join('\n') +} diff --git a/test/package.json b/test/package.json index e7abdd479..9a567deec 100644 --- a/test/package.json +++ b/test/package.json @@ -28,6 +28,7 @@ "@payloadcms/db-mongodb": "workspace:*", "@payloadcms/db-postgres": "workspace:*", "@payloadcms/db-sqlite": "workspace:*", + "@payloadcms/db-vercel-postgres": "workspace:*", "@payloadcms/drizzle": "workspace:*", "@payloadcms/email-nodemailer": "workspace:*", "@payloadcms/email-resend": "workspace:*", diff --git a/test/setupProd.ts b/test/setupProd.ts index 65ba4997e..6f94abefd 100644 --- a/test/setupProd.ts +++ b/test/setupProd.ts @@ -9,6 +9,7 @@ export const tgzToPkgNameMap = { payload: 'payload-*', '@payloadcms/db-mongodb': 'payloadcms-db-mongodb-*', '@payloadcms/db-postgres': 'payloadcms-db-postgres-*', + '@payloadcms/db-vercel-postgres': 'payloadcms-db-vercel-postgres-*', '@payloadcms/db-sqlite': 'payloadcms-db-sqlite-*', '@payloadcms/drizzle': 'payloadcms-drizzle-*', '@payloadcms/email-nodemailer': 'payloadcms-email-nodemailer-*',