From 72efd56302df59a6739668b3a0d2e2203ba30ef0 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 7 Aug 2023 14:40:00 -0400 Subject: [PATCH] chore: WIP create --- packages/db-postgres/package.json | 2 +- packages/db-postgres/src/create/index.ts | 13 +- packages/db-postgres/src/create/insertRows.ts | 181 ++++++++--- .../db-postgres/src/create/traverseFields.ts | 289 ++++++++++-------- packages/db-postgres/src/create/types.ts | 20 ++ packages/db-postgres/src/schema/build.ts | 2 +- .../src/transform/traverseFields.ts | 1 + packages/db-postgres/src/types.ts | 8 +- .../src/utilities/isArrayOfRows.ts | 3 + packages/db-postgres/yarn.lock | 8 +- 10 files changed, 348 insertions(+), 179 deletions(-) create mode 100644 packages/db-postgres/src/create/types.ts create mode 100644 packages/db-postgres/src/utilities/isArrayOfRows.ts diff --git a/packages/db-postgres/package.json b/packages/db-postgres/package.json index 99b4e83fe6..876128ba2f 100644 --- a/packages/db-postgres/package.json +++ b/packages/db-postgres/package.json @@ -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" }, diff --git a/packages/db-postgres/src/create/index.ts b/packages/db-postgres/src/create/index.ts index 189ffba223..0d2af5bfd4 100644 --- a/packages/db-postgres/src/create/index.ts +++ b/packages/db-postgres/src/create/index.ts @@ -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; }; diff --git a/packages/db-postgres/src/create/insertRows.ts b/packages/db-postgres/src/create/insertRows.ts index b78fe1978b..4130f8a7e3 100644 --- a/packages/db-postgres/src/create/insertRows.ts +++ b/packages/db-postgres/src/create/insertRows.ts @@ -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 + addRowIndexToPath?: boolean + rows: Record[] fallbackLocale?: string | false fields: Field[] + initialRowData?: Record[] + incomingRelationshipRows?: Record[] + 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> => { - const row: Record = {}; - const localeRow: Record = {}; - const relationshipRows: Record[] = []; +}: Args): Promise[]> => { + 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[] = []; + let insertedRelationshipRows: Record[] = []; + + 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 = { ...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; }); }; diff --git a/packages/db-postgres/src/create/traverseFields.ts b/packages/db-postgres/src/create/traverseFields.ts index 474671092d..77d3a670df 100644 --- a/packages/db-postgres/src/create/traverseFields.ts +++ b/packages/db-postgres/src/create/traverseFields.ts @@ -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 + fallbackLocale?: string | false fields: Field[] locale: string localeRow: Record + operation: 'create' | 'update' + path: string relationshipRows: Record[] row: Record 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[], + 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 = { - // _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> = { - // _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, + 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, + 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 = { + 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; } } - }); + })); }; diff --git a/packages/db-postgres/src/create/types.ts b/packages/db-postgres/src/create/types.ts new file mode 100644 index 0000000000..a2398ffbd9 --- /dev/null +++ b/packages/db-postgres/src/create/types.ts @@ -0,0 +1,20 @@ +import { Block } from 'payload/types'; + +export type ArrayRowPromise = (args: { parentID: string | number }) => Promise[]> + +export type ArrayRowPromisesMap = { + [tableName: string]: ArrayRowPromise +} + +export type BlockRowsToInsert = { + block: Block + rows: Record[] +} + +export type RowInsertionGroup = { + row: Record + localeRow: Record + relationshipRows: Record[] + arrayRowPromises: ArrayRowPromisesMap, + blockRows: { [blockType: string]: BlockRowsToInsert } +} diff --git a/packages/db-postgres/src/schema/build.ts b/packages/db-postgres/src/schema/build.ts index 2ad4860ec3..680072c6d2 100644 --- a/packages/db-postgres/src/schema/build.ts +++ b/packages/db-postgres/src/schema/build.ts @@ -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(); diff --git a/packages/db-postgres/src/transform/traverseFields.ts b/packages/db-postgres/src/transform/traverseFields.ts index 58781114e4..8e30fc1254 100644 --- a/packages/db-postgres/src/transform/traverseFields.ts +++ b/packages/db-postgres/src/transform/traverseFields.ts @@ -153,6 +153,7 @@ export const traverseFields = >({ case 'relationship': { const relationPathMatch = relationships[`${sanitizedPath}${field.name}`]; + if (!relationPathMatch) break; if (!field.hasMany) { const relation = relationPathMatch[0]; diff --git a/packages/db-postgres/src/types.ts b/packages/db-postgres/src/types.ts index e3552474d5..d8546943d8 100644 --- a/packages/db-postgres/src/types.ts +++ b/packages/db-postgres/src/types.ts @@ -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, { 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[]]> diff --git a/packages/db-postgres/src/utilities/isArrayOfRows.ts b/packages/db-postgres/src/utilities/isArrayOfRows.ts new file mode 100644 index 0000000000..3390d67739 --- /dev/null +++ b/packages/db-postgres/src/utilities/isArrayOfRows.ts @@ -0,0 +1,3 @@ +export function isArrayOfRows(data: unknown): data is Record[] { + return Array.isArray(data); +} diff --git a/packages/db-postgres/yarn.lock b/packages/db-postgres/yarn.lock index c8b6d5bbec..a6c367f7c5 100644 --- a/packages/db-postgres/yarn.lock +++ b/packages/db-postgres/yarn.lock @@ -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"