diff --git a/.vscode/launch.json b/.vscode/launch.json index 2441e9ace..35905ffea 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -25,5 +25,108 @@ "type": "node-terminal", "cwd": "${workspaceFolder}" }, + { + "type": "node", + "request": "launch", + "name": "Migrate CLI - create", + "runtimeArgs": [ + "-r", + "ts-node/register" + ], + "args": [ + "src/bin/migrate.ts", + "migrate:create", + "second" + ], + "env": { + "PAYLOAD_CONFIG_PATH": "test/migrations-cli/config.ts" + }, + "outputCapture": "std", + }, + { + "type": "node", + "request": "launch", + "name": "Migrate CLI - migrate", + "runtimeArgs": [ + "-r", + "ts-node/register" + ], + "args": [ + "src/bin/migrate.ts", + "migrate", + ], + "env": { + "PAYLOAD_CONFIG_PATH": "test/migrations-cli/config.ts" + }, + "outputCapture": "std", + }, + { + "type": "node", + "request": "launch", + "name": "Migrate CLI - status", + "runtimeArgs": [ + "-r", + "ts-node/register" + ], + "args": [ + "src/bin/migrate.ts", + "migrate:status", + ], + "env": { + "PAYLOAD_CONFIG_PATH": "test/migrations-cli/config.ts" + }, + "outputCapture": "std", + }, + { + "type": "node", + "request": "launch", + "name": "Migrate CLI - down", + "runtimeArgs": [ + "-r", + "ts-node/register" + ], + "args": [ + "src/bin/migrate.ts", + "migrate:down", + ], + "env": { + "PAYLOAD_CONFIG_PATH": "test/migrations-cli/config.ts" + }, + "outputCapture": "std", + }, + { + "type": "node", + "request": "launch", + "name": "Migrate CLI - reset", + "runtimeArgs": [ + "-r", + "ts-node/register" + ], + "args": [ + "src/bin/migrate.ts", + "migrate:reset", + ], + "env": { + "PAYLOAD_CONFIG_PATH": "test/migrations-cli/config.ts" + }, + "outputCapture": "std", + }, + { + "type": "node", + "request": "launch", + "name": "Migrate CLI - refresh", + "runtimeArgs": [ + "-r", + "ts-node/register" + ], + "args": [ + "src/bin/migrate.ts", + "migrate:refresh", + ], + "env": { + "PAYLOAD_CONFIG_PATH": "test/migrations-cli/config.ts" + }, + "outputCapture": "std", + }, ] } diff --git a/package.json b/package.json index 9944663c8..2b2508132 100644 --- a/package.json +++ b/package.json @@ -96,6 +96,7 @@ "compression": "^1.7.4", "conf": "^10.2.0", "connect-history-api-fallback": "^1.6.0", + "console-table-printer": "^2.11.1", "css-loader": "^5.2.7", "css-minimizer-webpack-plugin": "^5.0.0", "dataloader": "^2.1.0", diff --git a/src/bin/index.ts b/src/bin/index.ts index 0fb83f375..a0e8f9689 100755 --- a/src/bin/index.ts +++ b/src/bin/index.ts @@ -4,6 +4,7 @@ import swcRegister from '@swc/register'; import { getTsconfig as getTSconfig } from 'get-tsconfig'; import { generateTypes } from './generateTypes'; import { generateGraphQLSchema } from './generateGraphQLSchema'; +import { migrate } from './migrate'; const tsConfig = getTSconfig(); @@ -41,29 +42,31 @@ const { build } = require('./build'); const args = minimist(process.argv.slice(2)); -const scriptIndex = args._.findIndex( - (x) => x === 'build', -); +const scriptIndex = args._.findIndex((x) => x === 'build'); const script = scriptIndex === -1 ? args._[0] : args._[scriptIndex]; -switch (script.toLowerCase()) { - case 'build': { - build(); - break; - } +if (script.startsWith('migrate')) { + migrate(args._); +} else { + switch (script.toLowerCase()) { + case 'build': { + build(); + break; + } - case 'generate:types': { - generateTypes(); - break; - } + case 'generate:types': { + generateTypes(); + break; + } - case 'generate:graphqlschema': { - generateGraphQLSchema(); - break; - } + case 'generate:graphqlschema': { + generateGraphQLSchema(); + break; + } - default: - console.log(`Unknown script "${script}".`); - break; + default: + console.log(`Unknown script "${script}".`); + break; + } } diff --git a/src/bin/migrate.ts b/src/bin/migrate.ts new file mode 100755 index 000000000..5914956da --- /dev/null +++ b/src/bin/migrate.ts @@ -0,0 +1,50 @@ +import payload from '..'; + +export const migrate = async (args: string[]): Promise => { + // Barebones instance to access database adapter + await payload.init({ + secret: '--unused--', + local: true, + }); + + const adapter = payload.config.db; + + if (!adapter) { + throw new Error('No database adapter found'); + } + + switch (args[0]) { + case 'migrate': + await adapter.migrate(); + break; + case 'migrate:status': + await adapter.migrateStatus(); + break; + case 'migrate:down': + await adapter.migrateDown(); + break; + case 'migrate:refresh': + await adapter.migrateRefresh(); + break; + case 'migrate:reset': + await adapter.migrateReset(); + break; + case 'migrate:fresh': + await adapter.migrateFresh(); + break; + case 'migrate:create': + await adapter.createMigration(args[1]); + break; + + default: + throw new Error(`Unknown migration command: ${args[0]}`); + } +}; + +// when launched directly +if (module.id === require.main.id) { + const args = process.argv.slice(2); + migrate(args).then(() => { + process.exit(0); + }); +} diff --git a/src/collections/config/schema.ts b/src/collections/config/schema.ts index 326c4dc7d..0e089815d 100644 --- a/src/collections/config/schema.ts +++ b/src/collections/config/schema.ts @@ -29,10 +29,15 @@ const collectionSchema = joi.object().keys({ admin: joi.func(), }), defaultSort: joi.string(), - graphQL: joi.object().keys({ - singularName: joi.string(), - pluralName: joi.string(), - }), + graphQL: joi.alternatives().try( + joi.object().keys( + { + singularName: joi.string(), + pluralName: joi.string(), + }, + ), + joi.boolean(), + ), typescript: joi.object().keys({ interface: joi.string(), }), diff --git a/src/collections/config/types.ts b/src/collections/config/types.ts index e95df5959..fdd9fcd0d 100644 --- a/src/collections/config/types.ts +++ b/src/collections/config/types.ts @@ -269,7 +269,7 @@ export type CollectionConfig = { graphQL?: { singularName?: string pluralName?: string - } + } | false; /** * Options used in typescript generation */ diff --git a/src/collections/graphql/init.ts b/src/collections/graphql/init.ts index 269056113..1f130b4f3 100644 --- a/src/collections/graphql/init.ts +++ b/src/collections/graphql/init.ts @@ -41,6 +41,9 @@ function initCollectionsGraphQL(payload: Payload): void { versions, }, } = collection; + + if (!graphQL) return; + const { fields } = config; let singularName; diff --git a/src/config/sanitize.ts b/src/config/sanitize.ts index 8ce67e819..d1effe673 100644 --- a/src/config/sanitize.ts +++ b/src/config/sanitize.ts @@ -8,6 +8,7 @@ import sanitizeGlobals from '../globals/config/sanitize'; import checkDuplicateCollections from '../utilities/checkDuplicateCollections'; import { defaults } from './defaults'; import getPreferencesCollection from '../preferences/preferencesCollection'; +import { migrationsCollection } from '../database/migrations/migrationsCollection'; const sanitizeConfig = (config: Config): SanitizedConfig => { const sanitizedConfig = merge(defaults, config, { @@ -29,6 +30,8 @@ const sanitizeConfig = (config: Config): SanitizedConfig => { sanitizedConfig.collections.push(getPreferencesCollection(sanitizedConfig)); + sanitizedConfig.collections.push(migrationsCollection); + sanitizedConfig.collections = sanitizedConfig.collections.map((collection) => sanitizeCollection(sanitizedConfig, collection)); checkDuplicateCollections(sanitizedConfig.collections); diff --git a/src/config/schema.ts b/src/config/schema.ts index 33a3ccef0..2cc9cb178 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -34,6 +34,7 @@ export default joi.object({ return value; }), cookiePrefix: joi.string(), + db: joi.any(), routes: joi.object({ admin: joi.string(), api: joi.string(), diff --git a/src/config/types.ts b/src/config/types.ts index fc59f54fb..b63b79440 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -81,7 +81,7 @@ export type InitOptions = { /** Express app for Payload to use */ express?: Express; /** MongoDB connection URL, starts with `mongo` */ - mongoURL: string | false; + mongoURL?: string | false; /** Extra configuration options that will be passed to MongoDB */ mongoOptions?: ConnectOptions & { /** Set false to disable $facet aggregation in non-supporting databases, Defaults to true */ diff --git a/src/database/migrations/createMigration.ts b/src/database/migrations/createMigration.ts new file mode 100644 index 000000000..e9ad40cc8 --- /dev/null +++ b/src/database/migrations/createMigration.ts @@ -0,0 +1,32 @@ +/* eslint-disable no-restricted-syntax, no-await-in-loop */ +import fs from 'fs'; +import { migrationTemplate } from './migrationTemplate'; +import { Payload } from '../..'; + +type CreateMigrationArgs = { + payload: Payload + migrationDir: string + migrationName: string +} + +export async function createMigration({ payload, migrationDir, migrationName }: CreateMigrationArgs) { + const dir = migrationDir || '.migrations'; // TODO: Verify path after linking + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir); + } + + const [yyymmdd, hhmmss] = new Date().toISOString().split('T'); + const formattedDate = yyymmdd.replace(/\D/g, ''); + const formattedTime = hhmmss.split('.')[0].replace(/\D/g, ''); + + const timestamp = `${formattedDate}_${formattedTime}`; + + const formattedName = migrationName.replace(/\W/g, '_'); + const fileName = `${timestamp}_${formattedName}.ts`; + const filePath = `${dir}/${fileName}`; + fs.writeFileSync( + filePath, + migrationTemplate, + ); + payload.logger.info({ msg: `Migration created at ${filePath}` }); +} diff --git a/src/database/migrations/getMigrations.ts b/src/database/migrations/getMigrations.ts new file mode 100644 index 000000000..85b62efd0 --- /dev/null +++ b/src/database/migrations/getMigrations.ts @@ -0,0 +1,23 @@ +import { Payload } from '../..'; +import { MigrationData } from '../types'; + +export async function getMigrations({ + payload, +}: { + payload: Payload; +}): Promise<{ existingMigrations: MigrationData[], latestBatch: number }> { + const migrationQuery = await payload.find({ + collection: 'payload-migrations', + sort: '-name', + }); + + const existingMigrations = migrationQuery.docs as unknown as MigrationData[]; + + // Get the highest batch number from existing migrations + const latestBatch = existingMigrations?.[0]?.batch || 0; + + return { + existingMigrations, + latestBatch, + }; +} diff --git a/src/database/migrations/migrate.ts b/src/database/migrations/migrate.ts new file mode 100644 index 000000000..2b7e69b81 --- /dev/null +++ b/src/database/migrations/migrate.ts @@ -0,0 +1,40 @@ +/* eslint-disable no-restricted-syntax, no-await-in-loop */ +import { DatabaseAdapter } from '../types'; +import { getMigrations } from './getMigrations'; +import { readMigrationFiles } from './readMigrationFiles'; + +export async function migrate(this: DatabaseAdapter): Promise { + const { payload } = this; + const migrationFiles = await readMigrationFiles({ payload }); + const { existingMigrations, latestBatch } = await getMigrations({ payload }); + + const newBatch = latestBatch + 1; + + // Execute 'up' function for each migration sequentially + for (const migration of migrationFiles) { + const existingMigration = existingMigrations.find((existing) => existing.name === migration.name); + + // Run migration if not found in database + if (existingMigration) { + continue; // eslint-disable-line no-continue + } + + payload.logger.info({ msg: `Migrating: ${migration.name}` }); + const start = Date.now(); + try { + await migration.up({ payload }); + payload.logger.info({ msg: `Migrated: ${migration.name} (${Date.now() - start}ms)` }); + } catch (err: unknown) { + payload.logger.error({ msg: `Error running migration ${migration.name}`, err }); + throw err; + } + + await payload.create({ + collection: 'payload-migrations', + data: { + name: migration.name, + batch: newBatch, + }, + }); + } +} diff --git a/src/database/migrations/migrateDown.ts b/src/database/migrations/migrateDown.ts new file mode 100644 index 000000000..c39446a73 --- /dev/null +++ b/src/database/migrations/migrateDown.ts @@ -0,0 +1,44 @@ +/* eslint-disable no-restricted-syntax, no-await-in-loop */ +import { DatabaseAdapter } from '../types'; +import { getMigrations } from './getMigrations'; +import { readMigrationFiles } from './readMigrationFiles'; + +export async function migrateDown(this: DatabaseAdapter): Promise { + const { payload } = this; + const migrationFiles = await readMigrationFiles({ payload }); + + const { existingMigrations, latestBatch } = await getMigrations({ + payload, + }); + + + const migrationsToRollback = existingMigrations.filter((migration) => migration.batch === latestBatch); + if (!migrationsToRollback?.length) { + payload.logger.info({ msg: 'No migrations to rollback.' }); + return; + } + payload.logger.info({ msg: `Rolling back batch ${latestBatch} consisting of ${migrationsToRollback.length} migrations.` }); + + for (const migration of migrationsToRollback) { + const migrationFile = migrationFiles.find((m) => m.name === migration.name); + if (!migrationFile) { + throw new Error(`Migration ${migration.name} not found locally.`); + } + + try { + payload.logger.info({ msg: `Migrating: ${migrationFile.name}` }); + const start = Date.now(); + await migrationFile.down({ payload }); + + payload.logger.info({ msg: `Migrated: ${migrationFile.name} (${Date.now() - start}ms)` }); + + await payload.delete({ + collection: 'payload-migrations', + id: migration.id, + }); + } catch (err: unknown) { + payload.logger.error({ msg: `Error running migration ${migrationFile.name}`, err }); + throw err; + } + } +} diff --git a/src/database/migrations/migrateRefresh.ts b/src/database/migrations/migrateRefresh.ts new file mode 100644 index 000000000..eb86f7b5b --- /dev/null +++ b/src/database/migrations/migrateRefresh.ts @@ -0,0 +1,38 @@ +/* eslint-disable no-restricted-syntax, no-await-in-loop */ +import { DatabaseAdapter } from '../types'; +import { readMigrationFiles } from './readMigrationFiles'; + +/** + * Reset and re-run all migrations. + */ +export async function migrateRefresh(this: DatabaseAdapter) { + const { payload } = this; + const migrationFiles = await readMigrationFiles({ payload }); + + // Clear all migrations + await payload.delete({ + collection: 'payload-migrations', + where: {}, // All migrations + }); + + // Run all migrations + for (const migration of migrationFiles) { + payload.logger.info({ msg: `Migrating: ${migration.name}` }); + try { + const start = Date.now(); + await migration.up({ payload }); + payload.logger.info({ msg: `Migrated: ${migration.name} (${Date.now() - start}ms)` }); + } catch (err: unknown) { + payload.logger.error({ msg: `Error running migration ${migration.name}`, err }); + throw err; + } + + await payload.create({ + collection: 'payload-migrations', + data: { + name: migration.name, + executed: true, + }, + }); + } +} diff --git a/src/database/migrations/migrateReset.ts b/src/database/migrations/migrateReset.ts new file mode 100644 index 000000000..c00690dbb --- /dev/null +++ b/src/database/migrations/migrateReset.ts @@ -0,0 +1,42 @@ +/* eslint-disable no-restricted-syntax, no-await-in-loop */ +import { DatabaseAdapter } from '../types'; +import { getMigrations } from './getMigrations'; +import { readMigrationFiles } from './readMigrationFiles'; + +export async function migrateReset(this: DatabaseAdapter): Promise { + const { payload } = this; + const migrationFiles = await readMigrationFiles({ payload }); + + const { existingMigrations } = await getMigrations({ payload }); + + if (!existingMigrations?.length) { + payload.logger.info({ msg: 'No migrations to reset.' }); + return; + } + + // Rollback all migrations in order + for (const migration of migrationFiles) { + // Create or update migration in database + const existingMigration = existingMigrations.find((existing) => existing.name === migration.name); + if (existingMigration) { + payload.logger.info({ msg: `Migrating: ${migration.name}` }); + try { + const start = Date.now(); + await migration.down({ payload }); + payload.logger.info({ msg: `Migrated: ${migration.name} (${Date.now() - start}ms)` }); + } catch (err: unknown) { + payload.logger.error({ msg: `Error running migration ${migration.name}`, err }); + throw err; + } + + await payload.delete({ + collection: 'payload-migrations', + where: { + id: { + equals: existingMigration.id, + }, + }, + }); + } + } +} diff --git a/src/database/migrations/migrateStatus.ts b/src/database/migrations/migrateStatus.ts new file mode 100644 index 000000000..43133a102 --- /dev/null +++ b/src/database/migrations/migrateStatus.ts @@ -0,0 +1,31 @@ +import { Table } from 'console-table-printer'; +import { DatabaseAdapter } from '../types'; +import { readMigrationFiles } from './readMigrationFiles'; +import { getMigrations } from './getMigrations'; + +export async function migrateStatus(this: DatabaseAdapter): Promise { + const { payload } = this; + const migrationFiles = await readMigrationFiles({ payload }); + const { existingMigrations } = await getMigrations({ payload }); + + // Compare migration files to existing migrations + const statuses = migrationFiles.map((migration) => { + const existingMigration = existingMigrations.find( + (m) => m.name === migration.name, + ); + return { + Ran: existingMigration ? 'Yes' : 'No', + Name: migration.name, + Batch: existingMigration?.batch, + }; + }); + + const p = new Table(); + + statuses.forEach((s) => { + p.addRow(s, { + color: s.Ran === 'Yes' ? 'green' : 'red', + }); + }); + p.printTable(); +} diff --git a/src/database/migrations/migrationTemplate.ts b/src/database/migrations/migrationTemplate.ts new file mode 100644 index 000000000..1d4bfd311 --- /dev/null +++ b/src/database/migrations/migrationTemplate.ts @@ -0,0 +1,11 @@ +export const migrationTemplate = ` +import payload, { Payload } from 'payload'; + +export async function up(payload: Payload): Promise { + // Migration code +}; + +export async function down(payload: Payload): Promise { + // Migration code +}; +`; diff --git a/src/database/migrations/migrationsCollection.ts b/src/database/migrations/migrationsCollection.ts new file mode 100644 index 000000000..55d56d4a1 --- /dev/null +++ b/src/database/migrations/migrationsCollection.ts @@ -0,0 +1,39 @@ +import type { CollectionConfig } from '../../collections/config/types'; + +export const migrationsCollection: CollectionConfig = { + slug: 'payload-migrations', + admin: { + hidden: true, + }, + graphQL: false, + fields: [ + { + name: 'name', + type: 'text', + }, + { + name: 'batch', + type: 'number', + }, + // TODO: determine how schema will impact migration workflow + { + name: 'schema', + type: 'json', + }, + // TODO: do we need to persist the indexes separate from the schema? + // { + // name: 'indexes', + // type: 'array', + // fields: [ + // { + // name: 'index', + // type: 'text', + // }, + // { + // name: 'value', + // type: 'json', + // }, + // ], + // }, + ], +}; diff --git a/src/database/migrations/readMigrationFiles.ts b/src/database/migrations/readMigrationFiles.ts new file mode 100644 index 000000000..cc7c337cc --- /dev/null +++ b/src/database/migrations/readMigrationFiles.ts @@ -0,0 +1,27 @@ +import fs from 'fs'; +import path from 'path'; +import { Migration } from '../types'; +import { Payload } from '../../index'; + +/** + * Read the migration files from disk + */ +export const readMigrationFiles = async ({ + payload, +}: { + payload: Payload; +}): Promise => { + const { config } = payload; + const files = fs + .readdirSync(config.db.migrationDir) + .sort() + .map((file) => { + return path.resolve(config.db.migrationDir, file); + }); + return files.map((filePath) => { + // eslint-disable-next-line @typescript-eslint/no-var-requires,import/no-dynamic-require + const migration = require(filePath) as Migration; + migration.name = path.basename(filePath).split('.')?.[0]; + return migration; + }); +}; diff --git a/src/database/types.ts b/src/database/types.ts index 2fda3510c..4ff1d5b45 100644 --- a/src/database/types.ts +++ b/src/database/types.ts @@ -56,10 +56,15 @@ export interface DatabaseAdapter { webpack?: Webpack; // migrations + /** + * Path to read and write migration files from + */ + migrationDir: string; + /** * Output a migration file */ - createMigration: () => Promise; + createMigration: (migrationName: string) => Promise; /** * Run any migration up functions that have not yet been performed and update the status @@ -114,8 +119,8 @@ export interface DatabaseAdapter { queryDrafts: QueryDrafts; - // operations - collections - find: Find; + // operations + find: (args: FindArgs) => Promise>; findOne: FindOne; create: Create; @@ -234,6 +239,9 @@ export type DeleteVersionsArgs = { collection: string where: Where locale?: string + sort?: { + [key: string]: string + } }; export type CreateVersionArgs = { @@ -264,26 +272,55 @@ export type UpdateVersion = (args: UpdateVersionArgs) => Prom export type CreateArgs = { collection: string data: Record + draft?: boolean + locale?: string } export type Create = (args: CreateArgs) => Promise +export type UpdateArgs = { + collection: string + data: Record + where: Where + draft?: boolean + locale?: string +} + +export type Update = (args: UpdateArgs) => Promise + export type UpdateOneArgs = { - collection: string, - data: Record, - where: Where, + collection: string + data: Record + where: Where + draft?: boolean locale?: string } export type UpdateOne = (args: UpdateOneArgs) => Promise export type DeleteOneArgs = { - collection: string, - where: Where, + collection: string + where: Where } export type DeleteOne = (args: DeleteOneArgs) => Promise +export type DeleteManyArgs = { + collection: string + where: Where +} + +export type Migration = MigrationData & { + up: ({ payload }: { payload }) => Promise + down: ({ payload }: { payload }) => Promise +}; + +export type MigrationData = { + id: string + name: string + batch: number +} + export type BuildSchema = (args: { config: SanitizedConfig, fields: Field[], diff --git a/src/mongoose/index.ts b/src/mongoose/index.ts index 2932eaa28..74a3af30a 100644 --- a/src/mongoose/index.ts +++ b/src/mongoose/index.ts @@ -1,49 +1,62 @@ import type { ConnectOptions } from 'mongoose'; +import { CollectionModel } from '../collections/config/types'; +import { createMigration } from '../database/migrations/createMigration'; +import { migrate } from '../database/migrations/migrate'; +import { migrateDown } from '../database/migrations/migrateDown'; +import { migrateRefresh } from '../database/migrations/migrateRefresh'; +import { migrateReset } from '../database/migrations/migrateReset'; +import { migrateStatus } from '../database/migrations/migrateStatus'; import type { DatabaseAdapter } from '../database/types'; +import { GlobalModel } from '../globals/config/types'; import type { Payload } from '../index'; import { connect } from './connect'; -import { init } from './init'; -import { webpack } from './webpack'; -import { CollectionModel } from '../collections/config/types'; -import { queryDrafts } from './queryDrafts'; -import { GlobalModel } from '../globals/config/types'; -import { find } from './find'; import { create } from './create'; -import { updateOne } from './updateOne'; +import { find } from './find'; +import { findGlobalVersions } from './findGlobalVersions'; +import { findVersions } from './findVersions'; +import { init } from './init'; +import { queryDrafts } from './queryDrafts'; +import { webpack } from './webpack'; + +import { createGlobal } from './createGlobal'; +import { createVersion } from './createVersion'; import { deleteOne } from './deleteOne'; +import { deleteVersions } from './deleteVersions'; import { findGlobal } from './findGlobal'; import { findOne } from './findOne'; -import { findVersions } from './findVersions'; -import { findGlobalVersions } from './findGlobalVersions'; -import { deleteVersions } from './deleteVersions'; -import { createVersion } from './createVersion'; -import { updateVersion } from './updateVersion'; import { updateGlobal } from './updateGlobal'; -import { createGlobal } from './createGlobal'; +import { updateOne } from './updateOne'; +import { updateVersion } from './updateVersion'; export interface Args { - payload: Payload, + payload: Payload; /** The URL to connect to MongoDB */ - url: string + url: string; + migrationDir?: string; connectOptions?: ConnectOptions & { /** Set false to disable $facet aggregation in non-supporting databases, Defaults to true */ - useFacet?: boolean - } + useFacet?: boolean; + }; } export type MongooseAdapter = DatabaseAdapter & Args & { - mongoMemoryServer: any + mongoMemoryServer: any; collections: { - [slug: string]: CollectionModel - } - globals: GlobalModel + [slug: string]: CollectionModel; + }; + globals: GlobalModel; versions: { - [slug: string]: CollectionModel - } - } + [slug: string]: CollectionModel; + }; + }; -export function mongooseAdapter({ payload, url, connectOptions }: Args): MongooseAdapter { +export function mongooseAdapter({ + payload, + url, + connectOptions, + migrationDir = '.migrations', +}: Args): MongooseAdapter { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore return { @@ -55,12 +68,14 @@ export function mongooseAdapter({ payload, url, connectOptions }: Args): Mongoos connect, init, webpack, - migrate: async () => null, - migrateStatus: async () => null, - migrateDown: async () => null, - migrateRefresh: async () => null, - migrateReset: async () => null, + migrate, + migrateStatus, + migrateDown, + migrateRefresh, + migrateReset, migrateFresh: async () => null, + migrationDir, + createMigration: async (migrationName) => createMigration({ payload, migrationDir, migrationName }), transaction: async () => true, beginTransaction: async () => true, rollbackTransaction: async () => true, diff --git a/src/payload.ts b/src/payload.ts index 7d412c5a7..ada70fbe5 100644 --- a/src/payload.ts +++ b/src/payload.ts @@ -160,7 +160,7 @@ export class BasePayload { ); } - if (options.mongoURL !== false && typeof options.mongoURL !== 'string') { + if (!options.local && options.mongoURL !== false && typeof options.mongoURL !== 'string') { throw new Error('Error: missing MongoDB connection URL.'); } @@ -202,34 +202,37 @@ export class BasePayload { // THIS BLOCK IS TEMPORARY UNTIL 2.0.0 // We automatically add the Mongoose adapter // if there is no defined database adapter - if (this.mongoURL) { - mongoose.set('strictQuery', false); - - if (!this.config.db) { - this.config.db = mongooseAdapter({ - payload: this, - url: this.mongoURL, - connectOptions: options.mongoOptions, - }); - } + if (!this.config.db) { + this.config.db = mongooseAdapter({ + payload: this, + url: this.mongoURL ? this.mongoURL : '', + connectOptions: options.mongoOptions, + }); } this.db = this.config.db; - if (this.db?.connect) { - this.mongoMemoryServer = await this.db.connect({ config: this.config }); + this.db.payload = this; + + if (this.mongoURL || this.db.connect) { + mongoose.set('strictQuery', false); + if (this.db?.connect) { + this.mongoMemoryServer = await this.db.connect({ config: this.config }); + } } // Configure email service - const emailOptions = options.email ? { ...(options.email) } : this.config.email; + const emailOptions = options.email ? { ...options.email } : this.config.email; if (options.email && this.config.email) { - this.logger.warn('Email options provided in both init options and config. Using init options.'); + this.logger.warn( + 'Email options provided in both init options and config. Using init options.', + ); } this.emailOptions = emailOptions ?? emailDefaults; this.email = buildEmail(this.emailOptions, this.logger); this.sendEmail = sendEmail.bind(this); - if (!this.config.graphQL.disable) { + if (!this.config.graphQL.disable && !this.config.graphQL) { registerGraphQLSchema(this); } diff --git a/test/migrations-cli/config.ts b/test/migrations-cli/config.ts new file mode 100644 index 000000000..d7a0aacb5 --- /dev/null +++ b/test/migrations-cli/config.ts @@ -0,0 +1,52 @@ +/* eslint-disable no-console */ + +import path from 'path'; +import { buildConfig } from '../buildConfig'; +import { CollectionConfig } from '../../types'; +import { mongooseAdapter } from '../../src/mongoose'; +import payload from '../../src'; + +const Users: CollectionConfig = { + slug: 'users', + auth: true, + fields: [ + { + name: 'custom', + type: 'text', + }, + { + name: 'checkbox', + type: 'checkbox', + }, + ], +}; + +// // @ts-expect-error partial +// const mockAdapter: DatabaseAdapter = { +// // payload: undefined, +// migrationDir: path.resolve(__dirname, '.migrations'), +// migrateStatus: async () => console.log('TODO: migrateStatus not implemented.'), +// createMigration: async (): Promise => +// console.log('TODO: createMigration not implemented.'), +// migrate: async (): Promise => console.log('TODO: migrate not implemented.'), +// migrateDown: async (): Promise => +// console.log('TODO: migrateDown not implemented.'), +// migrateRefresh: async (): Promise => +// console.log('TODO: migrateRefresh not implemented.'), +// migrateReset: async (): Promise => +// console.log('TODO: migrateReset not implemented.'), +// migrateFresh: async (): Promise => +// console.log('TODO: migrateFresh not implemented.'), +// }; + +export default buildConfig({ + serverURL: 'http://localhost:3000', + admin: { + user: Users.slug, + }, + collections: [Users], + typescript: { + outputFile: path.resolve(__dirname, 'payload-types.ts'), + }, + db: mongooseAdapter({ payload, url: 'mongodb://localhost:27017/migrations-cli-test' }), +}); diff --git a/test/migrations-cli/payload-types.ts b/test/migrations-cli/payload-types.ts new file mode 100644 index 000000000..fc46a8729 --- /dev/null +++ b/test/migrations-cli/payload-types.ts @@ -0,0 +1,48 @@ +/* tslint:disable */ +/** + * This file was automatically generated by Payload CMS. + * DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config, + * and re-run `payload generate:types` to regenerate this file. + */ + +export interface Config { + collections: { + users: User; + 'payload-preferences': PayloadPreference; + }; + globals: {}; +} +export interface User { + id: string; + custom?: string; + checkbox?: boolean; + updatedAt: string; + createdAt: string; + email: string; + resetPasswordToken?: string; + resetPasswordExpiration?: string; + salt?: string; + hash?: string; + loginAttempts?: number; + lockUntil?: string; + password?: string; +} +export interface PayloadPreference { + id: string; + user: { + value: string | User; + relationTo: 'users'; + }; + key?: string; + value?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + updatedAt: string; + createdAt: string; +} diff --git a/yarn.lock b/yarn.lock index e1ad30aac..9070013ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4172,6 +4172,13 @@ connect-history-api-fallback@^1.6.0: resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== +console-table-printer@^2.11.1: + version "2.11.1" + resolved "https://registry.yarnpkg.com/console-table-printer/-/console-table-printer-2.11.1.tgz#c2dfe56e6343ea5bcfa3701a4be29fe912dbd9c7" + integrity sha512-8LfFpbF/BczoxPwo2oltto5bph8bJkGOATXsg3E9ddMJOGnWJciKHldx2zDj5XIBflaKzPfVCjOTl6tMh7lErg== + dependencies: + simple-wcswidth "^1.0.1" + content-disposition@0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" @@ -10829,6 +10836,11 @@ simple-update-notifier@^1.0.7: dependencies: semver "~7.0.0" +simple-wcswidth@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/simple-wcswidth/-/simple-wcswidth-1.0.1.tgz#8ab18ac0ae342f9d9b629604e54d2aa1ecb018b2" + integrity sha512-xMO/8eNREtaROt7tJvWJqHBDTMFN4eiQ5I4JRMuilwfnFcV5W9u7RUkueNkdw0jPqGMX36iCywelS5yilTuOxg== + sirv@^1.0.7: version "1.0.19" resolved "https://registry.yarnpkg.com/sirv/-/sirv-1.0.19.tgz#1d73979b38c7fe91fcba49c85280daa9c2363b49"