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",
|
"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",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
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(),
|
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(),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
|||||||
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;
|
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[],
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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"
|
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"
|
||||||
|
|||||||
Reference in New Issue
Block a user