chore: WIP create

This commit is contained in:
James
2023-08-07 14:40:00 -04:00
parent 6192c8623f
commit 72efd56302
10 changed files with 348 additions and 179 deletions

View File

@@ -14,7 +14,7 @@
},
"dependencies": {
"drizzle-kit": "^0.19.13-a511135",
"drizzle-orm": "^0.27.2",
"drizzle-orm": "^0.28.0",
"pg": "^8.11.1",
"to-snake-case": "^1.0.0"
},

View File

@@ -5,17 +5,18 @@ import { insertRows } from './insertRows';
export const create: Create = async function create({
collection: collectionSlug,
data,
// fallbackLocale,
locale,
req,
}) {
const collection = this.payload.collections[collectionSlug].config;
return insertRows({
const [result] = await insertRows({
adapter: this,
data,
fallbackLocale: false,
rows: [data],
fallbackLocale: req.fallbackLocale,
fields: collection.fields,
locale,
locale: req.locale,
tableName: toSnakeCase(collectionSlug),
});
return result;
};

View File

@@ -1,71 +1,168 @@
/* eslint-disable no-param-reassign */
import { Field } from 'payload/types';
import toSnakeCase from 'to-snake-case';
import { fieldAffectsData } from 'payload/dist/fields/config/types';
import { Block, fieldAffectsData } from 'payload/dist/fields/config/types';
import { PostgresAdapter } from '../types';
import { traverseFields } from './traverseFields';
import { transform } from '../transform';
import { ArrayRowPromisesMap, BlockRowsToInsert, RowInsertionGroup } from './types';
type Args = {
adapter: PostgresAdapter
data: Record<string, unknown>
addRowIndexToPath?: boolean
rows: Record<string, unknown>[]
fallbackLocale?: string | false
fields: Field[]
initialRowData?: Record<string, unknown>[]
incomingRelationshipRows?: Record<string, unknown>[]
incomingBlockRows?: { [blockType: string]: BlockRowsToInsert }
locale: string
operation: 'create' | 'update'
path?: string
tableName: string
}
export const insertRows = async ({
adapter,
data,
addRowIndexToPath,
rows,
fallbackLocale,
fields,
initialRowData,
incomingBlockRows,
incomingRelationshipRows,
locale,
operation,
path = '',
tableName,
}: Args): Promise<Record<string, unknown>> => {
const row: Record<string, unknown> = {};
const localeRow: Record<string, unknown> = {};
const relationshipRows: Record<string, unknown>[] = [];
}: Args): Promise<Record<string, unknown>[]> => {
const insertions: RowInsertionGroup[] = [];
await traverseFields({
adapter,
data,
fields,
locale,
localeRow,
relationshipRows,
row,
tableName,
await Promise.all(rows.map(async (data, i) => {
const insertion: RowInsertionGroup = {
row: { ...initialRowData?.[i] || {} },
localeRow: {},
relationshipRows: incomingRelationshipRows || [],
blockRows: incomingBlockRows || {},
arrayRowPromises: {},
};
await traverseFields({
adapter,
arrayRowPromises: insertion.arrayRowPromises,
blockRows: insertion.blockRows,
data,
fallbackLocale,
fields,
locale,
localeRow: insertion.localeRow,
operation,
path: addRowIndexToPath ? `${path}${i}.` : path,
relationshipRows: insertion.relationshipRows,
row: insertion.row,
tableName,
});
insertions.push(insertion);
}));
const insertedRows = await adapter.db.insert(adapter.tables[tableName])
.values(insertions.map(({ row }) => row)).returning();
let insertedLocaleRows: Record<string, unknown>[] = [];
let insertedRelationshipRows: Record<string, unknown>[] = [];
const relatedRowPromises = [];
// Fill related rows with parent IDs returned from database
insertedRows.forEach((row, i) => {
insertions[i].row = row;
const { localeRow, relationshipRows, blockRows, arrayRowPromises } = insertions[i];
if (Object.keys(arrayRowPromises).length > 0) {
Object.entries(arrayRowPromises).forEach(([key, func]) => {
relatedRowPromises.push(async () => {
insertions[i].row[key] = await func({ parentID: row.id as string });
});
});
}
if (!incomingBlockRows && Object.keys(blockRows).length > 0) {
Object.entries(blockRows).forEach(([blockType, { block, rows: blockRowsToInsert }]) => {
relatedRowPromises.push(async () => {
const result = await insertRows({
adapter,
addRowIndexToPath: true,
rows: blockRowsToInsert,
fallbackLocale,
fields: block.fields,
initialRowData: blockRowsToInsert.map((initialBlockRow) => ({
_order: initialBlockRow._order,
_parentID: row.id,
_path: initialBlockRow._path,
})),
incomingBlockRows,
incomingRelationshipRows,
locale,
operation,
path,
tableName: `${tableName}_${toSnakeCase(blockType)}`,
});
return result;
});
});
}
if (Object.keys(localeRow).length > 0) {
localeRow._parentID = row.id;
localeRow._locale = locale;
insertedLocaleRows.push(localeRow);
}
if (relationshipRows.length > 0) {
insertedRelationshipRows = insertedRelationshipRows.concat(relationshipRows.map((relationshipRow) => {
relationshipRow.parent = row.id;
return relationshipRow;
}));
}
});
const [insertedRow] = await adapter.db.insert(adapter.tables[tableName])
.values(row).returning();
const result: Record<string, unknown> = { ...insertedRow };
if (Object.keys(localeRow).length > 0) {
localeRow._parentID = insertedRow.id;
localeRow._locale = locale;
const [insertedLocaleRow] = await adapter.db.insert(adapter.tables[`${tableName}_locales`])
.values(localeRow).returning();
result._locales = insertedLocaleRow;
// Insert locales
if (insertedLocaleRows.length > 0) {
relatedRowPromises.push(async () => {
insertedLocaleRows = await adapter.db.insert(adapter.tables[`${tableName}_locales`])
.values(insertedLocaleRows).returning();
});
}
if (relationshipRows.length > 0) {
const insertedRelationshipRows = await adapter.db.insert(adapter.tables[`${tableName}_relationships`])
.values(relationshipRows.map((relationRow) => ({
...relationRow,
parent: insertedRow.id,
}))).returning();
result._relationships = insertedRelationshipRows;
// Insert relationships
// NOTE - only do this if there are no incoming relationship rows
// because `insertRows` is recursive and relationships should only happen at the top level
if (!incomingRelationshipRows && insertedRelationshipRows.length > 0) {
relatedRowPromises.push(async () => {
insertedRelationshipRows = await adapter.db.insert(adapter.tables[`${tableName}_relationships`])
.values(insertedRelationshipRows).returning();
});
}
return transform({
config: adapter.payload.config,
data: result,
fallbackLocale,
fields,
locale,
await Promise.all(relatedRowPromises.map((promise) => promise()));
return insertedRows.map((row) => {
const matchedLocaleRow = insertedLocaleRows.find(({ _parentID }) => _parentID === row.id);
if (matchedLocaleRow) row._locales = [matchedLocaleRow];
const matchedRelationshipRows = insertedRelationshipRows.filter(({ parent }) => parent === row.id);
if (matchedRelationshipRows.length > 0) row._relationships = matchedRelationshipRows;
const result = transform({
config: adapter.payload.config,
data: row,
fallbackLocale,
fields,
locale,
});
return result;
});
};

View File

@@ -1,15 +1,24 @@
/* eslint-disable no-param-reassign */
import { Field } from 'payload/types';
import toSnakeCase from 'to-snake-case';
import { fieldAffectsData } from 'payload/dist/fields/config/types';
import { fieldAffectsData, valueIsValueWithRelation } from 'payload/dist/fields/config/types';
import { PostgresAdapter } from '../types';
import { ArrayRowPromise, ArrayRowPromisesMap, BlockRowsToInsert } from './types';
import { insertRows } from './insertRows';
import { isArrayOfRows } from '../utilities/isArrayOfRows';
type Args = {
adapter: PostgresAdapter
arrayRowPromises: ArrayRowPromisesMap
blockRows: { [blockType: string]: BlockRowsToInsert }
columnPrefix?: string
data: Record<string, unknown>
fallbackLocale?: string | false
fields: Field[]
locale: string
localeRow: Record<string, unknown>
operation: 'create' | 'update'
path: string
relationshipRows: Record<string, unknown>[]
row: Record<string, unknown>
tableName: string
@@ -17,32 +26,44 @@ type Args = {
export const traverseFields = async ({
adapter,
arrayRowPromises,
blockRows,
columnPrefix,
data,
fallbackLocale,
fields,
locale,
localeRow,
operation,
path,
relationshipRows,
row,
tableName,
}: Args) => {
let targetRow = row;
fields.forEach((field) => {
await Promise.all(fields.map(async (field) => {
let targetRow = row;
let columnName: string;
let fieldData: unknown;
if (fieldAffectsData(field)) {
columnName = `${columnPrefix || ''}${toSnakeCase(field.name)}`;
columnName = `${columnPrefix || ''}${field.name}`;
fieldData = data[field.name];
if (field.localized) {
targetRow = localeRow;
if (typeof data[field.name] === 'object'
&& data[field.name] !== null
&& data[field.name][locale]) {
fieldData = data[field.name][locale];
}
}
}
switch (field.type) {
case 'number': {
// TODO: handle hasMany
targetRow[columnName] = data[columnName];
targetRow[columnName] = fieldData;
break;
}
@@ -51,150 +72,176 @@ export const traverseFields = async ({
}
case 'array': {
if (isArrayOfRows(fieldData)) {
const arrayTableName = `${tableName}_${toSnakeCase(field.name)}`;
const promise: ArrayRowPromise = async ({ parentID }) => {
const result = await insertRows({
adapter,
addRowIndexToPath: true,
fallbackLocale,
fields: field.fields,
incomingBlockRows: blockRows,
incomingRelationshipRows: relationshipRows,
initialRowData: (fieldData as []).map((_, i) => ({
_order: i + 1,
_parentID: parentID,
})),
locale,
operation,
rows: fieldData as Record<string, unknown>[],
tableName: arrayTableName,
});
return result.map((subRow) => {
delete subRow._order;
delete subRow._parentID;
return subRow;
});
};
arrayRowPromises[columnName] = promise;
}
break;
}
case 'blocks': {
// field.blocks.forEach((block) => {
// const baseColumns: Record<string, AnyPgColumnBuilder> = {
// _order: integer('_order').notNull(),
// _path: text('_path').notNull(),
// _parentID: parentIDColumnMap[parentIDColType]('_parent_id').references(() => adapter.tables[tableName].id).notNull(),
// };
if (isArrayOfRows(fieldData)) {
fieldData.forEach((blockRow, i) => {
if (typeof blockRow.blockType !== 'string') return;
const matchedBlock = field.blocks.find(({ slug }) => slug === blockRow.blockType);
if (!matchedBlock) return;
// if (field.localized && adapter.payload.config.localization) {
// baseColumns._locale = adapter.enums._locales('_locale').notNull();
// }
// const blockTableName = `${tableName}_${toSnakeCase(block.slug)}`;
// if (!adapter.tables[blockTableName]) {
// const { arrayBlockRelations: subArrayBlockRelations } = buildTable({
// adapter,
// baseColumns,
// fields: block.fields,
// tableName: blockTableName,
// });
// const blockTableRelations = relations(adapter.tables[blockTableName], ({ many, one }) => {
// const result: Record<string, Relation<string>> = {
// _parentID: one(adapter.tables[tableName], {
// fields: [adapter.tables[blockTableName]._parentID],
// references: [adapter.tables[tableName].id],
// }),
// };
// if (field.localized) {
// result._locales = many(adapter.tables[`${blockTableName}_locales`]);
// }
// subArrayBlockRelations.forEach((val, key) => {
// result[key] = many(adapter.tables[val]);
// });
// return result;
// });
// adapter.relations[blockTableName] = blockTableRelations;
// }
// arrayBlockRelations.set(`_${fieldPrefix || ''}${field.name}`, blockTableName);
// });
if (!blockRows[blockRow.blockType]) {
blockRows[blockRow.blockType] = {
rows: [],
block: matchedBlock,
};
}
blockRow._order = i + 1;
blockRow._path = `${path}${field.name}`;
blockRows[blockRow.blockType].rows.push(blockRow);
});
}
break;
}
case 'group': {
// Todo: determine what should happen if groups are set to localized
// const { hasLocalizedField: groupHasLocalizedField } = traverseFields({
// adapter,
// arrayBlockRelations,
// buildRelationships,
// columnPrefix: `${columnName}_`,
// columns,
// fieldPrefix: `${fieldPrefix || ''}${field.name}_`,
// fields: field.fields,
// indexes,
// localesColumns,
// localesIndexes,
// tableName,
// relationships,
// });
// if (groupHasLocalizedField) hasLocalizedField = true;
if (typeof data[field.name] === 'object' && data[field.name] !== null) {
await traverseFields({
adapter,
arrayRowPromises,
blockRows,
columnPrefix: `${columnName}_`,
data: data[field.name] as Record<string, unknown>,
fields: field.fields,
locale,
localeRow,
operation,
path: `${path || ''}${field.name}.`,
relationshipRows,
row,
tableName,
});
}
break;
}
case 'tabs': {
// field.tabs.forEach((tab) => {
// if ('name' in tab) {
// const { hasLocalizedField: tabHasLocalizedField } = traverseFields({
// adapter,
// arrayBlockRelations,
// buildRelationships,
// columnPrefix: `${columnName}_`,
// columns,
// fieldPrefix: `${fieldPrefix || ''}${tab.name}_`,
// fields: tab.fields,
// indexes,
// localesColumns,
// localesIndexes,
// tableName,
// relationships,
// });
// if (tabHasLocalizedField) hasLocalizedField = true;
// } else {
// ({ hasLocalizedField } = traverseFields({
// adapter,
// arrayBlockRelations,
// buildRelationships,
// columns,
// fields: tab.fields,
// indexes,
// localesColumns,
// localesIndexes,
// tableName,
// relationships,
// }));
// }
// });
await Promise.all(field.tabs.map(async (tab) => {
if ('name' in tab) {
if (typeof data[tab.name] === 'object' && data[tab.name] !== null) {
await traverseFields({
adapter,
arrayRowPromises,
blockRows,
columnPrefix: `${columnName}_`,
data: data[tab.name] as Record<string, unknown>,
fields: tab.fields,
locale,
localeRow,
operation,
path: `${path || ''}${tab.name}.`,
relationshipRows,
row,
tableName,
});
}
} else {
await traverseFields({
adapter,
arrayRowPromises,
blockRows,
columnPrefix,
data,
fields: tab.fields,
locale,
localeRow,
operation,
path,
relationshipRows,
row,
tableName,
});
}
}));
break;
}
case 'row':
case 'collapsible': {
// ({ hasLocalizedField } = traverseFields({
// adapter,
// arrayBlockRelations,
// buildRelationships,
// columns,
// fields: field.fields,
// indexes,
// localesColumns,
// localesIndexes,
// tableName,
// relationships,
// }));
await traverseFields({
adapter,
arrayRowPromises,
blockRows,
columnPrefix,
data,
fields: field.fields,
locale,
localeRow,
operation,
path,
relationshipRows,
row,
tableName,
});
break;
}
case 'relationship':
case 'upload':
// if (Array.isArray(field.relationTo)) {
// field.relationTo.forEach((relation) => relationships.add(relation));
// } else {
// relationships.add(field.relationTo);
// }
case 'upload': {
const relations = Array.isArray(fieldData) ? fieldData : [fieldData];
relations.forEach((relation, i) => {
const relationRow: Record<string, unknown> = {
path: `${path || ''}${field.name}`,
};
if ('hasMany' in field && field.hasMany) relationRow.order = i + 1;
if (field.localized) relationRow.locale = locale;
if (Array.isArray(field.relationTo) && valueIsValueWithRelation(relation)) {
relationRow[`${relation.relationTo}ID`] = relation.value;
relationshipRows.push(relationRow);
} else {
relationRow[`${field.relationTo}ID`] = relation;
relationshipRows.push(relationRow);
}
});
break;
}
default: {
if (typeof data[field.name] !== 'undefined') {
targetRow[field.name] = data[field.name];
if (typeof fieldData !== 'undefined') {
targetRow[columnName] = fieldData;
}
break;
}
}
});
}));
};

View File

@@ -0,0 +1,20 @@
import { Block } from 'payload/types';
export type ArrayRowPromise = (args: { parentID: string | number }) => Promise<Record<string, unknown>[]>
export type ArrayRowPromisesMap = {
[tableName: string]: ArrayRowPromise
}
export type BlockRowsToInsert = {
block: Block
rows: Record<string, unknown>[]
}
export type RowInsertionGroup = {
row: Record<string, unknown>
localeRow: Record<string, unknown>
relationshipRows: Record<string, unknown>[]
arrayRowPromises: ArrayRowPromisesMap,
blockRows: { [blockType: string]: BlockRowsToInsert }
}

View File

@@ -106,7 +106,7 @@ export const buildTable = ({
if (hasLocalizedField) {
const localeTableName = `${formattedTableName}_locales`;
localesColumns.id = integer('id').primaryKey();
localesColumns.id = serial('id').primaryKey();
localesColumns._locale = adapter.enums._locales('_locale').notNull();
localesColumns._parentID = parentIDColumnMap[idColType]('_parent_id').references(() => table.id).notNull();

View File

@@ -153,6 +153,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
case 'relationship': {
const relationPathMatch = relationships[`${sanitizedPath}${field.name}`];
if (!relationPathMatch) break;
if (!field.hasMany) {
const relation = relationPathMatch[0];

View File

@@ -1,6 +1,6 @@
import { Relation, Relations } from 'drizzle-orm';
import { ColumnBaseConfig, ColumnDataType, Relation, Relations } from 'drizzle-orm';
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import { PgColumn, PgColumnHKT, PgEnum, PgTableWithColumns } from 'drizzle-orm/pg-core';
import { PgColumn, PgEnum, PgTableWithColumns } from 'drizzle-orm/pg-core';
import { Payload } from 'payload';
import { DatabaseAdapter } from 'payload/dist/database/types';
import { ClientConfig, PoolConfig } from 'pg';
@@ -23,7 +23,7 @@ type PoolArgs = {
export type Args = ClientArgs | PoolArgs
export type GenericColumn = PgColumn<PgColumnHKT, {
export type GenericColumn = PgColumn<ColumnBaseConfig<ColumnDataType, string>, {
tableName: string;
name: string;
data: unknown;
@@ -37,7 +37,7 @@ export type GenericColumns = {
}
export type GenericTable = PgTableWithColumns<{
name: string, schema: undefined, columns: GenericColumns
name: string, schema: undefined, columns: GenericColumns, dialect: string
}>
export type GenericEnum = PgEnum<[string, ...string[]]>

View File

@@ -0,0 +1,3 @@
export function isArrayOfRows(data: unknown): data is Record<string, unknown>[] {
return Array.isArray(data);
}

View File

@@ -3120,10 +3120,10 @@ drizzle-kit@^0.19.13-a511135:
minimatch "^7.4.3"
zod "^3.20.2"
drizzle-orm@^0.27.2:
version "0.27.2"
resolved "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.27.2.tgz#bff4b9bb3dc53aa9f12ad2804bc8229f4c757cf8"
integrity sha512-ZvBvceff+JlgP7FxHKe0zOU9CkZ4RcOtibumIrqfYzDGuOeF0YUY0F9iMqYpRM7pxnLRfC+oO7rWOUH3T5oFQA==
drizzle-orm@^0.28.0:
version "0.28.0"
resolved "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.28.0.tgz#8412b32077028b664ce21fe946de0578af6a5837"
integrity sha512-iNNNtWM6YwXWI5vAkgFx+FPtZvo/ZnLh8uZV1e7+Alhan5ZS0q3tqbGFP8uCMngW+hNbJqsaHsxPw6AN87urmw==
duplexer@^0.1.2:
version "0.1.2"