chore: ports over find utilities
This commit is contained in:
@@ -1,10 +1,21 @@
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { sql, eq } from 'drizzle-orm';
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import type { Connect } from 'payload/dist/database/types';
|
||||
import { Client, Pool } from 'pg';
|
||||
import { pushSchema } from 'drizzle-kit/utils';
|
||||
import { configToJSONSchema } from 'payload/dist/utilities/configToJSONSchema';
|
||||
import prompts from 'prompts';
|
||||
|
||||
import type { PostgresAdapter } from '.';
|
||||
import { DrizzleDB } from './types';
|
||||
import { jsonb, numeric, pgTable, varchar } from 'drizzle-orm/pg-core';
|
||||
import type { PostgresAdapter } from './types';
|
||||
import { DrizzleDB, GenericEnum, GenericRelation, GenericTable } from './types';
|
||||
|
||||
// Migration table def in order to use query using drizzle
|
||||
const migrationsSchema = pgTable('payload_migrations', {
|
||||
name: varchar('name'),
|
||||
batch: numeric('batch'),
|
||||
schema: jsonb('schema'),
|
||||
});
|
||||
|
||||
export const connect: Connect = async function connect(
|
||||
this: PostgresAdapter,
|
||||
@@ -12,16 +23,30 @@ export const connect: Connect = async function connect(
|
||||
) {
|
||||
let db: DrizzleDB;
|
||||
|
||||
const schema: Record<string, GenericEnum | GenericTable | GenericRelation> = {};
|
||||
|
||||
Object.entries(this.tables).forEach(([key, val]) => {
|
||||
schema[`${key}`] = val;
|
||||
});
|
||||
|
||||
Object.entries(this.relations).forEach(([key, val]) => {
|
||||
schema[`${key}`] = val;
|
||||
});
|
||||
|
||||
Object.entries(this.enums).forEach(([key, val]) => {
|
||||
schema[`${key}`] = val;
|
||||
});
|
||||
|
||||
try {
|
||||
if ('pool' in this && this.pool !== false) {
|
||||
const pool = new Pool(this.pool);
|
||||
db = drizzle(pool);
|
||||
db = drizzle(pool, { schema });
|
||||
await pool.connect();
|
||||
}
|
||||
|
||||
if ('client' in this && this.client !== false) {
|
||||
const client = new Client(this.client);
|
||||
db = drizzle(client);
|
||||
db = drizzle(client, { schema });
|
||||
await client.connect();
|
||||
}
|
||||
|
||||
@@ -40,4 +65,75 @@ export const connect: Connect = async function connect(
|
||||
|
||||
this.payload.logger.info('Connected to Postgres successfully');
|
||||
this.db = db;
|
||||
|
||||
|
||||
// Only push schema if not in production
|
||||
if (process.env.NODE_ENV === 'production') return;
|
||||
|
||||
// This will prompt if clarifications are needed for Drizzle to push new schema
|
||||
const { hasDataLoss, warnings, statementsToExecute, apply } = await pushSchema(schema, this.db);
|
||||
|
||||
this.payload.logger.debug({
|
||||
msg: 'Schema push results',
|
||||
hasDataLoss,
|
||||
warnings,
|
||||
statementsToExecute,
|
||||
});
|
||||
|
||||
if (warnings.length) {
|
||||
this.payload.logger.warn({
|
||||
msg: `Warnings detected during schema push: ${warnings.join('\n')}`,
|
||||
warnings,
|
||||
});
|
||||
|
||||
if (hasDataLoss) {
|
||||
this.payload.logger.warn({
|
||||
msg: 'DATA LOSS WARNING: Possible data loss detected if schema is pushed.',
|
||||
});
|
||||
}
|
||||
|
||||
const { confirm: acceptWarnings } = await prompts(
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'confirm',
|
||||
message: 'Accept warnings and push schema to database?',
|
||||
initial: false,
|
||||
},
|
||||
{
|
||||
onCancel: () => {
|
||||
process.exit(0);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Exit if user does not accept warnings.
|
||||
// Q: Is this the right type of exit for this interaction?
|
||||
if (!acceptWarnings) {
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
const jsonSchema = configToJSONSchema(this.payload.config);
|
||||
|
||||
await apply();
|
||||
|
||||
const devPush = await this.db
|
||||
.select()
|
||||
.from(migrationsSchema)
|
||||
.where(eq(migrationsSchema.batch, '-1'));
|
||||
|
||||
if (!devPush.length) {
|
||||
await this.db.insert(migrationsSchema).values({
|
||||
name: 'dev',
|
||||
batch: '-1',
|
||||
schema: JSON.stringify(jsonSchema),
|
||||
});
|
||||
} else {
|
||||
await this.db
|
||||
.update(migrationsSchema)
|
||||
.set({
|
||||
schema: JSON.stringify(jsonSchema),
|
||||
})
|
||||
.where(eq(migrationsSchema.batch, '-1'));
|
||||
}
|
||||
};
|
||||
|
||||
71
packages/db-postgres/src/find/buildFindManyArgs.ts
Normal file
71
packages/db-postgres/src/find/buildFindManyArgs.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { ArrayField, Block } from 'payload/types';
|
||||
import { SanitizedCollectionConfig } from 'payload/dist/collections/config/types';
|
||||
import { SanitizedConfig } from 'payload/config';
|
||||
import { DBQueryConfig } from 'drizzle-orm';
|
||||
import { traverseFields } from './traverseFields';
|
||||
import { buildWithFromDepth } from './buildWithFromDepth';
|
||||
import { createLocaleWhereQuery } from './createLocaleWhereQuery';
|
||||
import { hasLocalesTable } from '../utilities/hasLocalesTable';
|
||||
|
||||
type BuildFindQueryArgs = {
|
||||
config: SanitizedConfig
|
||||
collection: SanitizedCollectionConfig
|
||||
depth: number
|
||||
fallbackLocale?: string | false
|
||||
locale?: string
|
||||
}
|
||||
|
||||
export type Result = DBQueryConfig<'many', true, any, any>
|
||||
|
||||
// Generate the Drizzle query for findMany based on
|
||||
// a collection field structure
|
||||
export const buildFindManyArgs = ({
|
||||
config,
|
||||
collection,
|
||||
depth,
|
||||
fallbackLocale,
|
||||
locale,
|
||||
}: BuildFindQueryArgs): Record<string, unknown> => {
|
||||
// In the future, we should remove hasLocalesTable here and just check for
|
||||
// the presence of the `${collectionSlug}_locales` table on the `db` -
|
||||
// that will be small perf enhancement
|
||||
const _locales = config.localization ? {
|
||||
where: createLocaleWhereQuery({ fallbackLocale, locale: locale || config.localization.defaultLocale }),
|
||||
columns: {
|
||||
id: false,
|
||||
_parentID: false,
|
||||
},
|
||||
} : undefined;
|
||||
|
||||
const result: Result = {
|
||||
with: {
|
||||
_relationships: {
|
||||
orderBy: ({ order }, { asc }) => [asc(order)],
|
||||
columns: {
|
||||
id: false,
|
||||
parent: false,
|
||||
},
|
||||
with: buildWithFromDepth({ config, depth, fallbackLocale, locale }),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (_locales && hasLocalesTable(collection.fields)) result.with._locales = _locales;
|
||||
|
||||
const locatedBlocks: Block[] = [];
|
||||
const locatedArrays: { [path: string]: ArrayField } = {};
|
||||
|
||||
traverseFields({
|
||||
config,
|
||||
currentArgs: result,
|
||||
depth,
|
||||
fields: collection.fields,
|
||||
_locales,
|
||||
locatedArrays,
|
||||
locatedBlocks,
|
||||
path: '',
|
||||
topLevelArgs: result,
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
37
packages/db-postgres/src/find/buildWithFromDepth.ts
Normal file
37
packages/db-postgres/src/find/buildWithFromDepth.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import { SanitizedConfig } from 'payload/config';
|
||||
import { buildFindManyArgs } from './buildFindManyArgs';
|
||||
|
||||
type BuildWithFromDepthArgs = {
|
||||
config: SanitizedConfig
|
||||
depth: number
|
||||
fallbackLocale?: string | false
|
||||
locale?: string
|
||||
}
|
||||
|
||||
export const buildWithFromDepth = ({
|
||||
config,
|
||||
depth,
|
||||
fallbackLocale,
|
||||
locale,
|
||||
}: BuildWithFromDepthArgs): Record<string, unknown> | undefined => {
|
||||
const result = config.collections.reduce((slugs, coll) => {
|
||||
const { slug } = coll;
|
||||
|
||||
if (depth >= 1) {
|
||||
const args = buildFindManyArgs({
|
||||
config,
|
||||
collection: coll,
|
||||
depth: depth - 1,
|
||||
fallbackLocale,
|
||||
locale,
|
||||
});
|
||||
|
||||
slugs[`${slug}ID`] = args;
|
||||
}
|
||||
|
||||
return slugs;
|
||||
}, {});
|
||||
|
||||
return result;
|
||||
};
|
||||
12
packages/db-postgres/src/find/createLocaleWhereQuery.ts
Normal file
12
packages/db-postgres/src/find/createLocaleWhereQuery.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
type createLocaleWhereQuery = {
|
||||
fallbackLocale?: string | false
|
||||
locale?: string
|
||||
}
|
||||
|
||||
export const createLocaleWhereQuery = ({ fallbackLocale, locale }) => {
|
||||
if (!locale || locale === 'all') return undefined;
|
||||
|
||||
if (fallbackLocale) return ({ _locale }, { or, eq }) => or(eq(_locale, locale), eq(_locale, fallbackLocale));
|
||||
|
||||
return ({ _locale }, { eq }) => eq(_locale, locale);
|
||||
};
|
||||
117
packages/db-postgres/src/find/traverseFields.ts
Normal file
117
packages/db-postgres/src/find/traverseFields.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import { SanitizedConfig } from 'payload/config';
|
||||
import { fieldAffectsData } from 'payload/dist/fields/config/types';
|
||||
import { ArrayField, Block, Field } from 'payload/types';
|
||||
import { hasLocalesTable } from '../utilities/hasLocalesTable';
|
||||
import { Result } from './buildFindManyArgs';
|
||||
|
||||
type TraverseFieldArgs = {
|
||||
config: SanitizedConfig,
|
||||
currentArgs: Record<string, unknown>,
|
||||
depth?: number,
|
||||
fields: Field[]
|
||||
_locales: Record<string, unknown>
|
||||
locatedArrays: { [path: string]: ArrayField },
|
||||
locatedBlocks: Block[],
|
||||
path: string,
|
||||
topLevelArgs: Record<string, unknown>,
|
||||
}
|
||||
|
||||
export const traverseFields = ({
|
||||
config,
|
||||
currentArgs,
|
||||
depth,
|
||||
fields,
|
||||
_locales,
|
||||
locatedArrays,
|
||||
locatedBlocks,
|
||||
path,
|
||||
topLevelArgs,
|
||||
}: TraverseFieldArgs) => {
|
||||
fields.forEach((field) => {
|
||||
if (fieldAffectsData(field)) {
|
||||
switch (field.type) {
|
||||
case 'array': {
|
||||
const withArray: Result = {
|
||||
orderBy: ({ _order }, { asc }) => [asc(_order)],
|
||||
columns: {
|
||||
_parentID: false,
|
||||
_order: false,
|
||||
},
|
||||
with: {},
|
||||
};
|
||||
|
||||
if (hasLocalesTable(field.fields) && _locales) withArray.with._locales = _locales;
|
||||
currentArgs.with[`${path}${field.name}`] = withArray;
|
||||
|
||||
traverseFields({
|
||||
config,
|
||||
currentArgs: withArray,
|
||||
depth,
|
||||
fields: field.fields,
|
||||
_locales,
|
||||
locatedArrays,
|
||||
locatedBlocks,
|
||||
path: '',
|
||||
topLevelArgs,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'blocks':
|
||||
field.blocks.forEach((block) => {
|
||||
const blockKey = `_blocks_${block.slug}`;
|
||||
|
||||
if (!topLevelArgs[blockKey]) {
|
||||
const withBlock: Result = {
|
||||
columns: {
|
||||
_parentID: false,
|
||||
},
|
||||
orderBy: ({ _order }, { asc }) => [asc(_order)],
|
||||
with: {},
|
||||
};
|
||||
|
||||
if (hasLocalesTable(block.fields) && _locales) withBlock.with._locales = _locales;
|
||||
topLevelArgs.with[blockKey] = withBlock;
|
||||
|
||||
traverseFields({
|
||||
config,
|
||||
currentArgs: withBlock,
|
||||
depth,
|
||||
fields: block.fields,
|
||||
_locales,
|
||||
locatedArrays,
|
||||
locatedBlocks,
|
||||
path,
|
||||
topLevelArgs,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
break;
|
||||
|
||||
case 'group':
|
||||
traverseFields({
|
||||
config,
|
||||
currentArgs,
|
||||
depth,
|
||||
fields: field.fields,
|
||||
_locales,
|
||||
locatedArrays,
|
||||
locatedBlocks,
|
||||
path: `${path}${field.name}_`,
|
||||
topLevelArgs,
|
||||
});
|
||||
|
||||
break;
|
||||
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return topLevelArgs;
|
||||
};
|
||||
@@ -3,6 +3,8 @@ import type { FindOne } from 'payload/dist/database/types';
|
||||
import type { PayloadRequest } from 'payload/dist/express/types';
|
||||
import type { SanitizedCollectionConfig } from 'payload/dist/collections/config/types';
|
||||
import buildQuery from './queries/buildQuery';
|
||||
import { buildFindManyArgs } from './find/buildFindManyArgs';
|
||||
import { transform } from './transform';
|
||||
|
||||
export const findOne: FindOne = async function findOne({
|
||||
collection,
|
||||
@@ -12,7 +14,6 @@ export const findOne: FindOne = async function findOne({
|
||||
}) {
|
||||
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config;
|
||||
const tableName = toSnakeCase(collection);
|
||||
const table = this.tables[tableName];
|
||||
|
||||
const query = await buildQuery({
|
||||
collectionSlug: collection,
|
||||
@@ -21,10 +22,25 @@ export const findOne: FindOne = async function findOne({
|
||||
where,
|
||||
});
|
||||
|
||||
const [doc] = await this.db.select()
|
||||
.from(table)
|
||||
.where(query)
|
||||
.limit(1);
|
||||
const findManyArgs = buildFindManyArgs({
|
||||
config: this.payload.config,
|
||||
collection: collectionConfig,
|
||||
depth: 0,
|
||||
fallbackLocale: req.fallbackLocale,
|
||||
locale: req.locale,
|
||||
});
|
||||
|
||||
return doc;
|
||||
findManyArgs.where = query;
|
||||
|
||||
const doc = await this.db.query[tableName].findFirst(findManyArgs);
|
||||
|
||||
const result = transform({
|
||||
config: this.payload.config,
|
||||
fallbackLocale: req.fallbackLocale,
|
||||
locale: req.locale,
|
||||
data: doc,
|
||||
fields: collectionConfig.fields,
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import { pushSchema } from 'drizzle-kit/utils';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { jsonb, numeric, pgEnum, pgTable, varchar } from 'drizzle-orm/pg-core';
|
||||
import { pgEnum } from 'drizzle-orm/pg-core';
|
||||
import { SanitizedCollectionConfig } from 'payload/dist/collections/config/types';
|
||||
import type { Init } from 'payload/dist/database/types';
|
||||
import { configToJSONSchema } from 'payload/dist/utilities/configToJSONSchema';
|
||||
import prompts from 'prompts';
|
||||
import { buildTable } from './schema/build';
|
||||
import type { GenericEnum, GenericRelation, GenericTable, PostgresAdapter } from './types';
|
||||
|
||||
// Migration table def in order to use query using drizzle
|
||||
const migrationsSchema = pgTable('payload_migrations', {
|
||||
name: varchar('name'),
|
||||
batch: numeric('batch'),
|
||||
schema: jsonb('schema'),
|
||||
});
|
||||
import type { PostgresAdapter } from './types';
|
||||
|
||||
export const init: Init = async function init(this: PostgresAdapter) {
|
||||
if (this.payload.config.localization) {
|
||||
@@ -37,88 +26,4 @@ export const init: Init = async function init(this: PostgresAdapter) {
|
||||
this.payload.config.globals.forEach((global) => {
|
||||
// create global model
|
||||
});
|
||||
|
||||
// Only push schema if not in production
|
||||
if (process.env.NODE_ENV === 'production') return;
|
||||
|
||||
const schema: Record<string, GenericEnum | GenericTable | GenericRelation> = {};
|
||||
|
||||
Object.entries(this.tables).forEach(([key, val]) => {
|
||||
schema[`table_${key}`] = val;
|
||||
});
|
||||
|
||||
Object.entries(this.relations).forEach(([key, val]) => {
|
||||
schema[`relation_${key}`] = val;
|
||||
});
|
||||
|
||||
Object.entries(this.enums).forEach(([key, val]) => {
|
||||
schema[`enum_${key}`] = val;
|
||||
});
|
||||
|
||||
// This will prompt if clarifications are needed for Drizzle to push new schema
|
||||
const { hasDataLoss, warnings, statementsToExecute, apply } = await pushSchema(schema, this.db);
|
||||
|
||||
this.payload.logger.debug({
|
||||
msg: 'Schema push results',
|
||||
hasDataLoss,
|
||||
warnings,
|
||||
statementsToExecute,
|
||||
});
|
||||
|
||||
if (warnings.length) {
|
||||
this.payload.logger.warn({
|
||||
msg: `Warnings detected during schema push: ${warnings.join('\n')}`,
|
||||
warnings,
|
||||
});
|
||||
|
||||
if (hasDataLoss) {
|
||||
this.payload.logger.warn({
|
||||
msg: 'DATA LOSS WARNING: Possible data loss detected if schema is pushed.',
|
||||
});
|
||||
}
|
||||
|
||||
const { confirm: acceptWarnings } = await prompts(
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'confirm',
|
||||
message: 'Accept warnings and push schema to database?',
|
||||
initial: false,
|
||||
},
|
||||
{
|
||||
onCancel: () => {
|
||||
process.exit(0);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Exit if user does not accept warnings.
|
||||
// Q: Is this the right type of exit for this interaction?
|
||||
if (!acceptWarnings) {
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
const jsonSchema = configToJSONSchema(this.payload.config);
|
||||
|
||||
await apply();
|
||||
|
||||
const devPush = await this.db
|
||||
.select()
|
||||
.from(migrationsSchema)
|
||||
.where(eq(migrationsSchema.batch, '-1'));
|
||||
|
||||
if (!devPush.length) {
|
||||
await this.db.insert(migrationsSchema).values({
|
||||
name: 'dev',
|
||||
batch: '-1',
|
||||
schema: JSON.stringify(jsonSchema),
|
||||
});
|
||||
} else {
|
||||
await this.db
|
||||
.update(migrationsSchema)
|
||||
.set({
|
||||
schema: JSON.stringify(jsonSchema),
|
||||
})
|
||||
.where(eq(migrationsSchema.batch, '-1'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -127,7 +127,7 @@ export const buildTable = ({
|
||||
}),
|
||||
}));
|
||||
|
||||
adapter.relations[localeTableName] = localesTableRelations;
|
||||
adapter.relations[`relations_${localeTableName}`] = localesTableRelations;
|
||||
}
|
||||
|
||||
if (buildRelationships) {
|
||||
@@ -184,7 +184,7 @@ export const buildTable = ({
|
||||
return result;
|
||||
});
|
||||
|
||||
adapter.relations[relationshipsTableName] = relationshipsTableRelations;
|
||||
adapter.relations[`relations_${relationshipsTableName}`] = relationshipsTableRelations;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +208,7 @@ export const buildTable = ({
|
||||
return result;
|
||||
});
|
||||
|
||||
adapter.relations[`${formattedTableName}`] = tableRelations;
|
||||
adapter.relations[`relations_${formattedTableName}`] = tableRelations;
|
||||
|
||||
return { arrayBlockRelations };
|
||||
};
|
||||
|
||||
@@ -155,7 +155,7 @@ export const traverseFields = ({
|
||||
return result;
|
||||
});
|
||||
|
||||
adapter.relations[arrayTableName] = arrayTableRelations;
|
||||
adapter.relations[`relations_${arrayTableName}`] = arrayTableRelations;
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -201,7 +201,7 @@ export const traverseFields = ({
|
||||
return result;
|
||||
});
|
||||
|
||||
adapter.relations[blockTableName] = blockTableRelations;
|
||||
adapter.relations[`relations_${blockTableName}`] = blockTableRelations;
|
||||
}
|
||||
|
||||
arrayBlockRelations.set(`_${fieldPrefix || ''}${field.name}`, blockTableName);
|
||||
|
||||
11
packages/db-postgres/src/utilities/hasLocalesTable.ts
Normal file
11
packages/db-postgres/src/utilities/hasLocalesTable.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { fieldAffectsData, fieldHasSubFields } from 'payload/dist/fields/config/types';
|
||||
import { Field } from 'payload/types';
|
||||
|
||||
export const hasLocalesTable = (fields: Field[]): boolean => {
|
||||
return fields.some((field) => {
|
||||
if (fieldAffectsData(field) && field.localized) return true;
|
||||
if (fieldHasSubFields(field) && field.type !== 'array') return hasLocalesTable(field.fields);
|
||||
if (field.type === 'tabs') return field.tabs.some((tab) => hasLocalesTable(tab.fields));
|
||||
return false;
|
||||
});
|
||||
};
|
||||
@@ -182,6 +182,10 @@ export class BasePayload<TGeneratedTypes extends GeneratedTypes> {
|
||||
this.db = this.config.db({ payload: this });
|
||||
this.db.payload = this;
|
||||
|
||||
if (this.db?.init) {
|
||||
await this.db.init(this);
|
||||
}
|
||||
|
||||
if (this.db.connect) {
|
||||
await this.db.connect(this);
|
||||
}
|
||||
@@ -202,10 +206,6 @@ export class BasePayload<TGeneratedTypes extends GeneratedTypes> {
|
||||
registerGraphQLSchema(this);
|
||||
}
|
||||
|
||||
if (this.db?.init) {
|
||||
await this.db.init(this);
|
||||
}
|
||||
|
||||
serverInitTelemetry(this);
|
||||
|
||||
if (options.local !== false) {
|
||||
|
||||
Reference in New Issue
Block a user