Feat/migrations cli (#2940)

* feat: migrate cli call db adapter

* feat: mongoose adapter migrate:create

* feat: implement migrate command

* feat: use mongooseAdapter in test config

* feat: use filename as migration name

* feat: intelligently execute migrations, status table

* feat: implement migrate:down

* feat: implement migrate:reset

* feat: implement migrate:refresh

* feat: move common adapter operations to database/migrations dir

* feat: delete migrations instead of storing ran property

* feat: createMigration cleanup

* feat: clean up logging and add duration to output

* chore: export type, handle graphQL false

* chore: simplify getting latest batch number

* chore: remove existing migration logging noise

* feat: remove adapter export from top level

* chore: fix some db types
This commit is contained in:
Elliot DeNolf
2023-07-07 11:02:56 -04:00
committed by GitHub
parent 756fe197c2
commit 2198445df9
26 changed files with 742 additions and 79 deletions

103
.vscode/launch.json vendored
View File

@@ -25,5 +25,108 @@
"type": "node-terminal", "type": "node-terminal",
"cwd": "${workspaceFolder}" "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",
},
] ]
} }

View File

@@ -96,6 +96,7 @@
"compression": "^1.7.4", "compression": "^1.7.4",
"conf": "^10.2.0", "conf": "^10.2.0",
"connect-history-api-fallback": "^1.6.0", "connect-history-api-fallback": "^1.6.0",
"console-table-printer": "^2.11.1",
"css-loader": "^5.2.7", "css-loader": "^5.2.7",
"css-minimizer-webpack-plugin": "^5.0.0", "css-minimizer-webpack-plugin": "^5.0.0",
"dataloader": "^2.1.0", "dataloader": "^2.1.0",

View File

@@ -4,6 +4,7 @@ import swcRegister from '@swc/register';
import { getTsconfig as getTSconfig } from 'get-tsconfig'; import { getTsconfig as getTSconfig } from 'get-tsconfig';
import { generateTypes } from './generateTypes'; import { generateTypes } from './generateTypes';
import { generateGraphQLSchema } from './generateGraphQLSchema'; import { generateGraphQLSchema } from './generateGraphQLSchema';
import { migrate } from './migrate';
const tsConfig = getTSconfig(); const tsConfig = getTSconfig();
@@ -41,29 +42,31 @@ const { build } = require('./build');
const args = minimist(process.argv.slice(2)); const args = minimist(process.argv.slice(2));
const scriptIndex = args._.findIndex( const scriptIndex = args._.findIndex((x) => x === 'build');
(x) => x === 'build',
);
const script = scriptIndex === -1 ? args._[0] : args._[scriptIndex]; const script = scriptIndex === -1 ? args._[0] : args._[scriptIndex];
switch (script.toLowerCase()) { if (script.startsWith('migrate')) {
case 'build': { migrate(args._);
build(); } else {
break; switch (script.toLowerCase()) {
} case 'build': {
build();
break;
}
case 'generate:types': { case 'generate:types': {
generateTypes(); generateTypes();
break; break;
} }
case 'generate:graphqlschema': { case 'generate:graphqlschema': {
generateGraphQLSchema(); generateGraphQLSchema();
break; break;
} }
default: default:
console.log(`Unknown script "${script}".`); console.log(`Unknown script "${script}".`);
break; break;
}
} }

50
src/bin/migrate.ts Executable file
View File

@@ -0,0 +1,50 @@
import payload from '..';
export const migrate = async (args: string[]): Promise<void> => {
// 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);
});
}

View File

@@ -29,10 +29,15 @@ const collectionSchema = joi.object().keys({
admin: joi.func(), admin: joi.func(),
}), }),
defaultSort: joi.string(), defaultSort: joi.string(),
graphQL: joi.object().keys({ graphQL: joi.alternatives().try(
singularName: joi.string(), joi.object().keys(
pluralName: joi.string(), {
}), singularName: joi.string(),
pluralName: joi.string(),
},
),
joi.boolean(),
),
typescript: joi.object().keys({ typescript: joi.object().keys({
interface: joi.string(), interface: joi.string(),
}), }),

View File

@@ -269,7 +269,7 @@ export type CollectionConfig = {
graphQL?: { graphQL?: {
singularName?: string singularName?: string
pluralName?: string pluralName?: string
} } | false;
/** /**
* Options used in typescript generation * Options used in typescript generation
*/ */

View File

@@ -41,6 +41,9 @@ function initCollectionsGraphQL(payload: Payload): void {
versions, versions,
}, },
} = collection; } = collection;
if (!graphQL) return;
const { fields } = config; const { fields } = config;
let singularName; let singularName;

View File

@@ -8,6 +8,7 @@ import sanitizeGlobals from '../globals/config/sanitize';
import checkDuplicateCollections from '../utilities/checkDuplicateCollections'; import checkDuplicateCollections from '../utilities/checkDuplicateCollections';
import { defaults } from './defaults'; import { defaults } from './defaults';
import getPreferencesCollection from '../preferences/preferencesCollection'; import getPreferencesCollection from '../preferences/preferencesCollection';
import { migrationsCollection } from '../database/migrations/migrationsCollection';
const sanitizeConfig = (config: Config): SanitizedConfig => { const sanitizeConfig = (config: Config): SanitizedConfig => {
const sanitizedConfig = merge(defaults, config, { const sanitizedConfig = merge(defaults, config, {
@@ -29,6 +30,8 @@ const sanitizeConfig = (config: Config): SanitizedConfig => {
sanitizedConfig.collections.push(getPreferencesCollection(sanitizedConfig)); sanitizedConfig.collections.push(getPreferencesCollection(sanitizedConfig));
sanitizedConfig.collections.push(migrationsCollection);
sanitizedConfig.collections = sanitizedConfig.collections.map((collection) => sanitizeCollection(sanitizedConfig, collection)); sanitizedConfig.collections = sanitizedConfig.collections.map((collection) => sanitizeCollection(sanitizedConfig, collection));
checkDuplicateCollections(sanitizedConfig.collections); checkDuplicateCollections(sanitizedConfig.collections);

View File

@@ -34,6 +34,7 @@ export default joi.object({
return value; return value;
}), }),
cookiePrefix: joi.string(), cookiePrefix: joi.string(),
db: joi.any(),
routes: joi.object({ routes: joi.object({
admin: joi.string(), admin: joi.string(),
api: joi.string(), api: joi.string(),

View File

@@ -81,7 +81,7 @@ export type InitOptions = {
/** Express app for Payload to use */ /** Express app for Payload to use */
express?: Express; express?: Express;
/** MongoDB connection URL, starts with `mongo` */ /** MongoDB connection URL, starts with `mongo` */
mongoURL: string | false; mongoURL?: string | false;
/** Extra configuration options that will be passed to MongoDB */ /** Extra configuration options that will be passed to MongoDB */
mongoOptions?: ConnectOptions & { mongoOptions?: ConnectOptions & {
/** Set false to disable $facet aggregation in non-supporting databases, Defaults to true */ /** Set false to disable $facet aggregation in non-supporting databases, Defaults to true */

View File

@@ -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}` });
}

View File

@@ -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,
};
}

View File

@@ -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<void> {
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,
},
});
}
}

View File

@@ -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<void> {
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;
}
}
}

View File

@@ -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,
},
});
}
}

View File

@@ -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<void> {
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,
},
},
});
}
}
}

View File

@@ -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<void> {
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();
}

View File

@@ -0,0 +1,11 @@
export const migrationTemplate = `
import payload, { Payload } from 'payload';
export async function up(payload: Payload): Promise<void> {
// Migration code
};
export async function down(payload: Payload): Promise<void> {
// Migration code
};
`;

View File

@@ -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',
// },
// ],
// },
],
};

View File

@@ -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<Migration[]> => {
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;
});
};

View File

@@ -56,10 +56,15 @@ export interface DatabaseAdapter {
webpack?: Webpack; webpack?: Webpack;
// migrations // migrations
/**
* Path to read and write migration files from
*/
migrationDir: string;
/** /**
* Output a migration file * Output a migration file
*/ */
createMigration: () => Promise<void>; createMigration: (migrationName: string) => Promise<void>;
/** /**
* Run any migration up functions that have not yet been performed and update the status * Run any migration up functions that have not yet been performed and update the status
@@ -114,8 +119,8 @@ export interface DatabaseAdapter {
queryDrafts: QueryDrafts; queryDrafts: QueryDrafts;
// operations - collections // operations
find: Find; find: <T = TypeWithID>(args: FindArgs) => Promise<PaginatedDocs<T>>;
findOne: FindOne; findOne: FindOne;
create: Create; create: Create;
@@ -234,6 +239,9 @@ export type DeleteVersionsArgs = {
collection: string collection: string
where: Where where: Where
locale?: string locale?: string
sort?: {
[key: string]: string
}
}; };
export type CreateVersionArgs<T = TypeWithID> = { export type CreateVersionArgs<T = TypeWithID> = {
@@ -264,26 +272,55 @@ export type UpdateVersion = <T = TypeWithID>(args: UpdateVersionArgs<T>) => Prom
export type CreateArgs = { export type CreateArgs = {
collection: string collection: string
data: Record<string, unknown> data: Record<string, unknown>
draft?: boolean
locale?: string
} }
export type Create = (args: CreateArgs) => Promise<Document> export type Create = (args: CreateArgs) => Promise<Document>
export type UpdateArgs = {
collection: string
data: Record<string, unknown>
where: Where
draft?: boolean
locale?: string
}
export type Update = (args: UpdateArgs) => Promise<Document>
export type UpdateOneArgs = { export type UpdateOneArgs = {
collection: string, collection: string
data: Record<string, unknown>, data: Record<string, unknown>
where: Where, where: Where
draft?: boolean
locale?: string locale?: string
} }
export type UpdateOne = (args: UpdateOneArgs) => Promise<Document> export type UpdateOne = (args: UpdateOneArgs) => Promise<Document>
export type DeleteOneArgs = { export type DeleteOneArgs = {
collection: string, collection: string
where: Where, where: Where
} }
export type DeleteOne = (args: DeleteOneArgs) => Promise<Document> export type DeleteOne = (args: DeleteOneArgs) => Promise<Document>
export type DeleteManyArgs = {
collection: string
where: Where
}
export type Migration = MigrationData & {
up: ({ payload }: { payload }) => Promise<boolean>
down: ({ payload }: { payload }) => Promise<boolean>
};
export type MigrationData = {
id: string
name: string
batch: number
}
export type BuildSchema<TSchema> = (args: { export type BuildSchema<TSchema> = (args: {
config: SanitizedConfig, config: SanitizedConfig,
fields: Field[], fields: Field[],

View File

@@ -1,49 +1,62 @@
import type { ConnectOptions } from 'mongoose'; 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 type { DatabaseAdapter } from '../database/types';
import { GlobalModel } from '../globals/config/types';
import type { Payload } from '../index'; import type { Payload } from '../index';
import { connect } from './connect'; 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 { 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 { deleteOne } from './deleteOne';
import { deleteVersions } from './deleteVersions';
import { findGlobal } from './findGlobal'; import { findGlobal } from './findGlobal';
import { findOne } from './findOne'; 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 { updateGlobal } from './updateGlobal';
import { createGlobal } from './createGlobal'; import { updateOne } from './updateOne';
import { updateVersion } from './updateVersion';
export interface Args { export interface Args {
payload: Payload, payload: Payload;
/** The URL to connect to MongoDB */ /** The URL to connect to MongoDB */
url: string url: string;
migrationDir?: string;
connectOptions?: ConnectOptions & { connectOptions?: ConnectOptions & {
/** Set false to disable $facet aggregation in non-supporting databases, Defaults to true */ /** Set false to disable $facet aggregation in non-supporting databases, Defaults to true */
useFacet?: boolean useFacet?: boolean;
} };
} }
export type MongooseAdapter = DatabaseAdapter & export type MongooseAdapter = DatabaseAdapter &
Args & { Args & {
mongoMemoryServer: any mongoMemoryServer: any;
collections: { collections: {
[slug: string]: CollectionModel [slug: string]: CollectionModel;
} };
globals: GlobalModel globals: GlobalModel;
versions: { 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 // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
return { return {
@@ -55,12 +68,14 @@ export function mongooseAdapter({ payload, url, connectOptions }: Args): Mongoos
connect, connect,
init, init,
webpack, webpack,
migrate: async () => null, migrate,
migrateStatus: async () => null, migrateStatus,
migrateDown: async () => null, migrateDown,
migrateRefresh: async () => null, migrateRefresh,
migrateReset: async () => null, migrateReset,
migrateFresh: async () => null, migrateFresh: async () => null,
migrationDir,
createMigration: async (migrationName) => createMigration({ payload, migrationDir, migrationName }),
transaction: async () => true, transaction: async () => true,
beginTransaction: async () => true, beginTransaction: async () => true,
rollbackTransaction: async () => true, rollbackTransaction: async () => true,

View File

@@ -160,7 +160,7 @@ export class BasePayload<TGeneratedTypes extends GeneratedTypes> {
); );
} }
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.'); throw new Error('Error: missing MongoDB connection URL.');
} }
@@ -202,34 +202,37 @@ export class BasePayload<TGeneratedTypes extends GeneratedTypes> {
// THIS BLOCK IS TEMPORARY UNTIL 2.0.0 // THIS BLOCK IS TEMPORARY UNTIL 2.0.0
// We automatically add the Mongoose adapter // We automatically add the Mongoose adapter
// if there is no defined database adapter // if there is no defined database adapter
if (this.mongoURL) { if (!this.config.db) {
mongoose.set('strictQuery', false); this.config.db = mongooseAdapter({
payload: this,
if (!this.config.db) { url: this.mongoURL ? this.mongoURL : '',
this.config.db = mongooseAdapter({ connectOptions: options.mongoOptions,
payload: this, });
url: this.mongoURL,
connectOptions: options.mongoOptions,
});
}
} }
this.db = this.config.db; this.db = this.config.db;
if (this.db?.connect) { this.db.payload = this;
this.mongoMemoryServer = await this.db.connect({ config: this.config });
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 // 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) { 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.emailOptions = emailOptions ?? emailDefaults;
this.email = buildEmail(this.emailOptions, this.logger); this.email = buildEmail(this.emailOptions, this.logger);
this.sendEmail = sendEmail.bind(this); this.sendEmail = sendEmail.bind(this);
if (!this.config.graphQL.disable) { if (!this.config.graphQL.disable && !this.config.graphQL) {
registerGraphQLSchema(this); registerGraphQLSchema(this);
} }

View File

@@ -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<void> =>
// console.log('TODO: createMigration not implemented.'),
// migrate: async (): Promise<void> => console.log('TODO: migrate not implemented.'),
// migrateDown: async (): Promise<void> =>
// console.log('TODO: migrateDown not implemented.'),
// migrateRefresh: async (): Promise<void> =>
// console.log('TODO: migrateRefresh not implemented.'),
// migrateReset: async (): Promise<void> =>
// console.log('TODO: migrateReset not implemented.'),
// migrateFresh: async (): Promise<void> =>
// 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' }),
});

View File

@@ -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;
}

View File

@@ -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" resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc"
integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== 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: content-disposition@0.5.4:
version "0.5.4" version "0.5.4"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" 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: dependencies:
semver "~7.0.0" 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: sirv@^1.0.7:
version "1.0.19" version "1.0.19"
resolved "https://registry.yarnpkg.com/sirv/-/sirv-1.0.19.tgz#1d73979b38c7fe91fcba49c85280daa9c2363b49" resolved "https://registry.yarnpkg.com/sirv/-/sirv-1.0.19.tgz#1d73979b38c7fe91fcba49c85280daa9c2363b49"