chore: ports over find utilities

This commit is contained in:
James
2023-08-09 16:41:21 -04:00
parent 4f7d7a4d1b
commit 2f8549f331
11 changed files with 382 additions and 117 deletions

View File

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

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

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

View 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);
};

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

View File

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

View File

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

View File

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

View File

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

View 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;
});
};

View File

@@ -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) {