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:
103
.vscode/launch.json
vendored
103
.vscode/launch.json
vendored
@@ -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",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
50
src/bin/migrate.ts
Executable file
50
src/bin/migrate.ts
Executable 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);
|
||||
});
|
||||
}
|
||||
@@ -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(),
|
||||
}),
|
||||
|
||||
@@ -269,7 +269,7 @@ export type CollectionConfig = {
|
||||
graphQL?: {
|
||||
singularName?: string
|
||||
pluralName?: string
|
||||
}
|
||||
} | false;
|
||||
/**
|
||||
* Options used in typescript generation
|
||||
*/
|
||||
|
||||
@@ -41,6 +41,9 @@ function initCollectionsGraphQL(payload: Payload): void {
|
||||
versions,
|
||||
},
|
||||
} = collection;
|
||||
|
||||
if (!graphQL) return;
|
||||
|
||||
const { fields } = config;
|
||||
|
||||
let singularName;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 */
|
||||
|
||||
32
src/database/migrations/createMigration.ts
Normal file
32
src/database/migrations/createMigration.ts
Normal 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}` });
|
||||
}
|
||||
23
src/database/migrations/getMigrations.ts
Normal file
23
src/database/migrations/getMigrations.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
40
src/database/migrations/migrate.ts
Normal file
40
src/database/migrations/migrate.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
44
src/database/migrations/migrateDown.ts
Normal file
44
src/database/migrations/migrateDown.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
38
src/database/migrations/migrateRefresh.ts
Normal file
38
src/database/migrations/migrateRefresh.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
42
src/database/migrations/migrateReset.ts
Normal file
42
src/database/migrations/migrateReset.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
31
src/database/migrations/migrateStatus.ts
Normal file
31
src/database/migrations/migrateStatus.ts
Normal 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();
|
||||
}
|
||||
11
src/database/migrations/migrationTemplate.ts
Normal file
11
src/database/migrations/migrationTemplate.ts
Normal 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
|
||||
};
|
||||
`;
|
||||
39
src/database/migrations/migrationsCollection.ts
Normal file
39
src/database/migrations/migrationsCollection.ts
Normal 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',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
],
|
||||
};
|
||||
27
src/database/migrations/readMigrationFiles.ts
Normal file
27
src/database/migrations/readMigrationFiles.ts
Normal 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;
|
||||
});
|
||||
};
|
||||
@@ -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<void>;
|
||||
createMigration: (migrationName: string) => Promise<void>;
|
||||
|
||||
/**
|
||||
* 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: <T = TypeWithID>(args: FindArgs) => Promise<PaginatedDocs<T>>;
|
||||
findOne: FindOne;
|
||||
|
||||
create: Create;
|
||||
@@ -234,6 +239,9 @@ export type DeleteVersionsArgs = {
|
||||
collection: string
|
||||
where: Where
|
||||
locale?: string
|
||||
sort?: {
|
||||
[key: string]: string
|
||||
}
|
||||
};
|
||||
|
||||
export type CreateVersionArgs<T = TypeWithID> = {
|
||||
@@ -264,26 +272,55 @@ export type UpdateVersion = <T = TypeWithID>(args: UpdateVersionArgs<T>) => Prom
|
||||
export type CreateArgs = {
|
||||
collection: string
|
||||
data: Record<string, unknown>
|
||||
draft?: boolean
|
||||
locale?: string
|
||||
}
|
||||
|
||||
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 = {
|
||||
collection: string,
|
||||
data: Record<string, unknown>,
|
||||
where: Where,
|
||||
collection: string
|
||||
data: Record<string, unknown>
|
||||
where: Where
|
||||
draft?: boolean
|
||||
locale?: string
|
||||
}
|
||||
|
||||
export type UpdateOne = (args: UpdateOneArgs) => Promise<Document>
|
||||
|
||||
export type DeleteOneArgs = {
|
||||
collection: string,
|
||||
where: Where,
|
||||
collection: string
|
||||
where: Where
|
||||
}
|
||||
|
||||
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: {
|
||||
config: SanitizedConfig,
|
||||
fields: Field[],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
|
||||
@@ -202,34 +202,37 @@ export class BasePayload<TGeneratedTypes extends GeneratedTypes> {
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
||||
52
test/migrations-cli/config.ts
Normal file
52
test/migrations-cli/config.ts
Normal 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' }),
|
||||
});
|
||||
48
test/migrations-cli/payload-types.ts
Normal file
48
test/migrations-cli/payload-types.ts
Normal 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;
|
||||
}
|
||||
12
yarn.lock
12
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"
|
||||
|
||||
Reference in New Issue
Block a user