diff --git a/.migrations/drizzle-snapshot.json b/.migrations/drizzle-snapshot.json new file mode 100644 index 0000000000..7011c2298d --- /dev/null +++ b/.migrations/drizzle-snapshot.json @@ -0,0 +1,935 @@ +{ + "id": "4b6f2243-b055-45d8-9afa-53cd2b729a12", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "5", + "dialect": "pg", + "tables": { + "posts_my_array_my_sub_array": { + "name": "posts_my_array_my_sub_array", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "sub_sub_field": { + "name": "sub_sub_field", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "posts_my_array_my_sub_array__parent_id_posts_my_array_id_fk": { + "name": "posts_my_array_my_sub_array__parent_id_posts_my_array_id_fk", + "tableFrom": "posts_my_array_my_sub_array", + "tableTo": "posts_my_array", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "posts_my_array": { + "name": "posts_my_array", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "posts_my_array__parent_id_posts_id_fk": { + "name": "posts_my_array__parent_id_posts_id_fk", + "tableFrom": "posts_my_array", + "tableTo": "posts", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "posts_my_array_locales": { + "name": "posts_my_array_locales", + "schema": "", + "columns": { + "sub_field": { + "name": "sub_field", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "_locale": { + "name": "_locale", + "type": "_locales", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "posts_my_array_locales__parent_id_posts_my_array_id_fk": { + "name": "posts_my_array_locales__parent_id_posts_my_array_id_fk", + "tableFrom": "posts_my_array_locales", + "tableTo": "posts_my_array", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "posts_block1": { + "name": "posts_block1", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_path": { + "name": "_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "non_localized_text": { + "name": "non_localized_text", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "block_name": { + "name": "block_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "posts_block1__parent_id_posts_id_fk": { + "name": "posts_block1__parent_id_posts_id_fk", + "tableFrom": "posts_block1", + "tableTo": "posts", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "posts_block1_locales": { + "name": "posts_block1_locales", + "schema": "", + "columns": { + "localized_text": { + "name": "localized_text", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "_locale": { + "name": "_locale", + "type": "_locales", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "posts_block1_locales__parent_id_posts_block1_id_fk": { + "name": "posts_block1_locales__parent_id_posts_block1_id_fk", + "tableFrom": "posts_block1_locales", + "tableTo": "posts_block1", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "posts_block2_block_array": { + "name": "posts_block2_block_array", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "sub_block_array": { + "name": "sub_block_array", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "posts_block2_block_array__parent_id_posts_block2_id_fk": { + "name": "posts_block2_block_array__parent_id_posts_block2_id_fk", + "tableFrom": "posts_block2_block_array", + "tableTo": "posts_block2", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "posts_block2": { + "name": "posts_block2", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_path": { + "name": "_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "number": { + "name": "number", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "block_name": { + "name": "block_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "posts_block2__parent_id_posts_id_fk": { + "name": "posts_block2__parent_id_posts_id_fk", + "tableFrom": "posts_block2", + "tableTo": "posts", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "posts_my_group_group_array": { + "name": "posts_my_group_group_array", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "group_array_text": { + "name": "group_array_text", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "posts_my_group_group_array__parent_id_posts_id_fk": { + "name": "posts_my_group_group_array__parent_id_posts_id_fk", + "tableFrom": "posts_my_group_group_array", + "tableTo": "posts", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "posts": { + "name": "posts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "my_group_sub_field": { + "name": "my_group_sub_field", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "my_group_sub_group_sub_sub_field": { + "name": "my_group_sub_group_sub_sub_field", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "posts_locales": { + "name": "posts_locales", + "schema": "", + "columns": { + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "number": { + "name": "number", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "my_group_sub_field_localized": { + "name": "my_group_sub_field_localized", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "my_group_sub_group_sub_sub_field_localized": { + "name": "my_group_sub_group_sub_sub_field_localized", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "_locale": { + "name": "_locale", + "type": "_locales", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "posts_locales__parent_id_posts_id_fk": { + "name": "posts_locales__parent_id_posts_id_fk", + "tableFrom": "posts_locales", + "tableTo": "posts", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "posts_relationships": { + "name": "posts_relationships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pages_id": { + "name": "pages_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "people_id": { + "name": "people_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "posts_id": { + "name": "posts_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "posts_relationships_parent_id_posts_id_fk": { + "name": "posts_relationships_parent_id_posts_id_fk", + "tableFrom": "posts_relationships", + "tableTo": "posts", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "posts_relationships_pages_id_pages_id_fk": { + "name": "posts_relationships_pages_id_pages_id_fk", + "tableFrom": "posts_relationships", + "tableTo": "pages", + "columnsFrom": [ + "pages_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "posts_relationships_people_id_people_id_fk": { + "name": "posts_relationships_people_id_people_id_fk", + "tableFrom": "posts_relationships", + "tableTo": "people", + "columnsFrom": [ + "people_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "posts_relationships_posts_id_posts_id_fk": { + "name": "posts_relationships_posts_id_posts_id_fk", + "tableFrom": "posts_relationships", + "tableTo": "posts", + "columnsFrom": [ + "posts_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "pages": { + "name": "pages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "people": { + "name": "people", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "full_name": { + "name": "full_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "reset_password_token": { + "name": "reset_password_token", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "salt": { + "name": "salt", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "hash": { + "name": "hash", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "login_attempts": { + "name": "login_attempts", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "email_idx": { + "name": "email_idx", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "payload_preferences": { + "name": "payload_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "payload_preferences_relationships": { + "name": "payload_preferences_relationships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "users_id": { + "name": "users_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "payload_preferences_relationships_parent_id_payload_preferences_id_fk": { + "name": "payload_preferences_relationships_parent_id_payload_preferences_id_fk", + "tableFrom": "payload_preferences_relationships", + "tableTo": "payload_preferences", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "payload_preferences_relationships_users_id_users_id_fk": { + "name": "payload_preferences_relationships_users_id_users_id_fk", + "tableFrom": "payload_preferences_relationships", + "tableTo": "users", + "columnsFrom": [ + "users_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "payload_migrations": { + "name": "payload_migrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "batch": { + "name": "batch", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": { + "_locales": { + "name": "_locales", + "values": { + "en": "en", + "es": "es" + } + } + }, + "schemas": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} \ No newline at end of file diff --git a/packages/db-postgres/src/create/index.ts b/packages/db-postgres/src/create/index.ts index 572ba5b052..77c69f80ba 100644 --- a/packages/db-postgres/src/create/index.ts +++ b/packages/db-postgres/src/create/index.ts @@ -1,6 +1,6 @@ import { Create } from 'payload/dist/database/types'; import toSnakeCase from 'to-snake-case'; -import { upsertRow } from '../upsertRow'; +import { insertRow } from '../insertRow'; export const create: Create = async function create({ collection: collectionSlug, @@ -9,13 +9,12 @@ export const create: Create = async function create({ }) { const collection = this.payload.collections[collectionSlug].config; - const result = await upsertRow({ + const result = await insertRow({ adapter: this, data, fallbackLocale: req.fallbackLocale, fields: collection.fields, locale: req.locale, - operation: 'create', tableName: toSnakeCase(collectionSlug), }); diff --git a/packages/db-postgres/src/findOne.ts b/packages/db-postgres/src/findOne.ts index 3f0c139965..ea51792578 100644 --- a/packages/db-postgres/src/findOne.ts +++ b/packages/db-postgres/src/findOne.ts @@ -4,7 +4,7 @@ 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'; +import { transform } from './transform/read'; export const findOne: FindOne = async function findOne({ collection, diff --git a/packages/db-postgres/src/upsertRow/insertArrays.ts b/packages/db-postgres/src/insertArrays.ts similarity index 97% rename from packages/db-postgres/src/upsertRow/insertArrays.ts rename to packages/db-postgres/src/insertArrays.ts index 92c21f815f..d82bb63b14 100644 --- a/packages/db-postgres/src/upsertRow/insertArrays.ts +++ b/packages/db-postgres/src/insertArrays.ts @@ -1,6 +1,6 @@ /* eslint-disable no-param-reassign */ -import { PostgresAdapter } from '../types'; -import { ArrayRowToInsert } from './types'; +import { PostgresAdapter } from './types'; +import { ArrayRowToInsert } from './transform/write/types'; type Args = { adapter: PostgresAdapter diff --git a/packages/db-postgres/src/insertRow/index.ts b/packages/db-postgres/src/insertRow/index.ts new file mode 100644 index 0000000000..c793861cf1 --- /dev/null +++ b/packages/db-postgres/src/insertRow/index.ts @@ -0,0 +1,185 @@ +/* eslint-disable no-param-reassign */ +import { Field } from 'payload/types'; +import { PostgresAdapter } from '../types'; +import { transform } from '../transform/read'; +import { BlockRowToInsert } from '../transform/write/types'; +import { insertArrays } from '../insertArrays'; +import { transformForWrite } from '../transform/write'; + +type Args = { + adapter: PostgresAdapter + data: Record + fallbackLocale?: string | false + fields: Field[] + locale: string + path?: string + tableName: string +} + +export const insertRow = async ({ + adapter, + data, + fallbackLocale, + fields, + locale, + path = '', + tableName, +}: Args): Promise> => { + // Split out the incoming data into the corresponding: + // base row, locales, relationships, blocks, and arrays + const rowToInsert = transformForWrite({ + data, + fields, + locale, + path, + tableName, + }); + + // First, we insert the main row + const [insertedRow] = await adapter.db.insert(adapter.tables[tableName]) + .values(rowToInsert.row).returning(); + + + let localeToInsert: Record; + const relationsToInsert: Record[] = []; + const blocksToInsert: { [blockType: string]: BlockRowToInsert[] } = {}; + + // Maintain a list of promises to run locale, blocks, and relationships + // all in parallel + const promises = []; + + // If there is a locale row with data, add the parent and locale + if (Object.keys(rowToInsert.locale).length > 0) { + rowToInsert.locale._parentID = insertedRow.id; + rowToInsert.locale._locale = locale; + localeToInsert = rowToInsert.locale; + } + + // If there are relationships, add parent to each + if (rowToInsert.relationships.length > 0) { + rowToInsert.relationships.forEach((relation) => { + relation.parent = insertedRow.id; + relationsToInsert.push(relation); + }); + } + + // If there are blocks, add parent to each, and then + // store by table name and rows + Object.keys(rowToInsert.blocks).forEach((blockName) => { + rowToInsert.blocks[blockName].forEach((blockRow) => { + blockRow.row._parentID = insertedRow.id; + if (!blocksToInsert[blockName]) blocksToInsert[blockName] = []; + blocksToInsert[blockName].push(blockRow); + }); + }); + + // ////////////////////////////////// + // INSERT LOCALES + // ////////////////////////////////// + + let insertedLocaleRow; + + if (localeToInsert) { + promises.push(async () => { + [insertedLocaleRow] = await adapter.db.insert(adapter.tables[`${tableName}_locales`]) + .values(localeToInsert).returning(); + }); + } + + // ////////////////////////////////// + // INSERT RELATIONSHIPS + // ////////////////////////////////// + + let insertedRelationshipRows: Record[]; + + if (relationsToInsert.length > 0) { + promises.push(async () => { + insertedRelationshipRows = await adapter.db.insert(adapter.tables[`${tableName}_relationships`]) + .values(relationsToInsert).returning(); + }); + } + + // ////////////////////////////////// + // INSERT BLOCKS + // ////////////////////////////////// + + const insertedBlockRows: Record[]> = {}; + + Object.entries(blocksToInsert).forEach(([blockName, blockRows]) => { + // For each block, push insert into promises to run parallel + promises.push(async () => { + insertedBlockRows[blockName] = await adapter.db.insert(adapter.tables[`${tableName}_${blockName}`]) + .values(blockRows.map(({ row }) => row)).returning(); + + insertedBlockRows[blockName].forEach((row, i) => { + delete row._parentID; + blockRows[i].row = row; + }); + + const blockLocaleIndexMap: number[] = []; + + const blockLocaleRowsToInsert = blockRows.reduce((acc, blockRow, i) => { + if (Object.keys(blockRow.locale).length > 0) { + blockRow.locale._parentID = blockRow.row.id; + blockRow.locale._locale = locale; + acc.push(blockRow.locale); + blockLocaleIndexMap.push(i); + return acc; + } + + return acc; + }, []); + + if (blockLocaleRowsToInsert.length > 0) { + const insertedBlockLocaleRows = await adapter.db.insert(adapter.tables[`${tableName}_${blockName}_locales`]) + .values(blockLocaleRowsToInsert).returning(); + + insertedBlockLocaleRows.forEach((blockLocaleRow, i) => { + delete blockLocaleRow._parentID; + insertedBlockRows[blockName][blockLocaleIndexMap[i]]._locales = [blockLocaleRow]; + }); + } + + await insertArrays({ + adapter, + arrays: blockRows.map(({ arrays }) => arrays), + parentRows: insertedBlockRows[blockName], + }); + }); + }); + + // ////////////////////////////////// + // INSERT ARRAYS RECURSIVELY + // ////////////////////////////////// + + promises.push(async () => { + await insertArrays({ + adapter, + arrays: [rowToInsert.arrays], + parentRows: [insertedRow], + }); + }); + + await Promise.all(promises.map((promise) => promise())); + + // ////////////////////////////////// + // TRANSFORM DATA + // ////////////////////////////////// + + if (insertedLocaleRow) insertedRow._locales = [insertedLocaleRow]; + if (insertedRelationshipRows?.length > 0) insertedRow._relationships = insertedRelationshipRows; + + Object.entries(insertedBlockRows).forEach(([blockName, blocks]) => { + if (blocks.length > 0) insertedRow[`_blocks_${blockName}`] = blocks; + }); + + const result = transform({ + config: adapter.payload.config, + data: insertedRow, + fallbackLocale, + fields, + locale, + }); + + return result; +}; diff --git a/packages/db-postgres/src/schema/build.ts b/packages/db-postgres/src/schema/build.ts index 74115aea1a..be1e2cac84 100644 --- a/packages/db-postgres/src/schema/build.ts +++ b/packages/db-postgres/src/schema/build.ts @@ -9,6 +9,7 @@ import { numeric, timestamp, IndexBuilder, + unique, } from 'drizzle-orm/pg-core'; import { Field } from 'payload/types'; import toSnakeCase from 'to-snake-case'; @@ -109,7 +110,9 @@ export const buildTable = ({ return Object.entries(localesIndexes).reduce((acc, [colName, func]) => { acc[colName] = func(cols); return acc; - }, {}); + }, { + _localeParent: unique().on(cols._locale, cols._parentID), + }); }); adapter.tables[localeTableName] = localesTable; diff --git a/packages/db-postgres/src/transform/index.ts b/packages/db-postgres/src/transform/read/index.ts similarity index 89% rename from packages/db-postgres/src/transform/index.ts rename to packages/db-postgres/src/transform/read/index.ts index 333adeaa08..6c38d106ca 100644 --- a/packages/db-postgres/src/transform/index.ts +++ b/packages/db-postgres/src/transform/read/index.ts @@ -3,9 +3,9 @@ import { Field } from 'payload/types'; import { TypeWithID } from 'payload/dist/collections/config/types'; import { SanitizedConfig } from 'payload/config'; import { traverseFields } from './traverseFields'; -import { createRelationshipMap } from '../utilities/createRelationshipMap'; +import { createRelationshipMap } from '../../utilities/createRelationshipMap'; import { mergeLocales } from './mergeLocales'; -import { createBlocksMap } from '../utilities/createBlocksMap'; +import { createBlocksMap } from '../../utilities/createBlocksMap'; type TransformArgs = { config: SanitizedConfig diff --git a/packages/db-postgres/src/transform/mergeLocales.ts b/packages/db-postgres/src/transform/read/mergeLocales.ts similarity index 100% rename from packages/db-postgres/src/transform/mergeLocales.ts rename to packages/db-postgres/src/transform/read/mergeLocales.ts diff --git a/packages/db-postgres/src/transform/traverseFields.ts b/packages/db-postgres/src/transform/read/traverseFields.ts similarity index 100% rename from packages/db-postgres/src/transform/traverseFields.ts rename to packages/db-postgres/src/transform/read/traverseFields.ts diff --git a/packages/db-postgres/src/transform/write/index.ts b/packages/db-postgres/src/transform/write/index.ts new file mode 100644 index 0000000000..8d97929afa --- /dev/null +++ b/packages/db-postgres/src/transform/write/index.ts @@ -0,0 +1,49 @@ +/* eslint-disable no-param-reassign */ +import { Field } from 'payload/types'; +import { traverseFields } from './traverseFields'; +import { RowToInsert } from './types'; + +type Args = { + data: Record + fields: Field[] + locale: string + path?: string + tableName: string +} + +export const transformForWrite = ({ + data, + fields, + locale, + path = '', + tableName, +}: Args): RowToInsert => { + // Split out the incoming data into the corresponding: + // base row, locales, relationships, blocks, and arrays + const rowToInsert: RowToInsert = { + row: {}, + locale: {}, + relationships: [], + blocks: {}, + arrays: {}, + }; + + // This function is responsible for building up the + // above rowToInsert + traverseFields({ + arrays: rowToInsert.arrays, + blocks: rowToInsert.blocks, + columnPrefix: '', + data, + fields, + locale, + localeRow: rowToInsert.locale, + newTableName: tableName, + parentTableName: tableName, + path, + relationships: rowToInsert.relationships, + row: rowToInsert.row, + }); + + return rowToInsert; +}; diff --git a/packages/db-postgres/src/upsertRow/traverseFields.ts b/packages/db-postgres/src/transform/write/traverseFields.ts similarity index 96% rename from packages/db-postgres/src/upsertRow/traverseFields.ts rename to packages/db-postgres/src/transform/write/traverseFields.ts index df06f9c4e7..f3812f627b 100644 --- a/packages/db-postgres/src/upsertRow/traverseFields.ts +++ b/packages/db-postgres/src/transform/write/traverseFields.ts @@ -2,12 +2,10 @@ import { Field } from 'payload/types'; import toSnakeCase from 'to-snake-case'; import { fieldAffectsData, valueIsValueWithRelation } from 'payload/dist/fields/config/types'; -import { PostgresAdapter } from '../types'; import { ArrayRowToInsert, BlockRowToInsert } from './types'; -import { isArrayOfRows } from '../utilities/isArrayOfRows'; +import { isArrayOfRows } from '../../utilities/isArrayOfRows'; type Args = { - adapter: PostgresAdapter arrays: { [tableName: string]: ArrayRowToInsert[] } @@ -27,7 +25,6 @@ type Args = { } export const traverseFields = ({ - adapter, arrays, blocks, columnPrefix, @@ -92,7 +89,6 @@ export const traverseFields = ({ if (field.localized) newRow.row._locale = locale; traverseFields({ - adapter, arrays: newRow.arrays, blocks, columnPrefix: '', @@ -137,7 +133,6 @@ export const traverseFields = ({ const blockTableName = `${newTableName}_${toSnakeCase(blockRow.blockType)}`; traverseFields({ - adapter, arrays: newRow.arrays, blocks, columnPrefix: '', @@ -162,7 +157,6 @@ export const traverseFields = ({ case 'group': { if (typeof data[field.name] === 'object' && data[field.name] !== null) { traverseFields({ - adapter, arrays, blocks, columnPrefix: `${columnName}_`, @@ -268,7 +262,7 @@ export const traverseFields = ({ relationships.push(relationRow); } else { relationRow[`${field.relationTo}ID`] = relation; - relationships.push(relationRow); + if (relation) relationships.push(relationRow); } }); diff --git a/packages/db-postgres/src/upsertRow/types.ts b/packages/db-postgres/src/transform/write/types.ts similarity index 100% rename from packages/db-postgres/src/upsertRow/types.ts rename to packages/db-postgres/src/transform/write/types.ts diff --git a/packages/db-postgres/src/update/index.ts b/packages/db-postgres/src/update/index.ts index d70229d038..0677435b80 100644 --- a/packages/db-postgres/src/update/index.ts +++ b/packages/db-postgres/src/update/index.ts @@ -1,34 +1,40 @@ import { UpdateOne } from 'payload/dist/database/types'; import toSnakeCase from 'to-snake-case'; -import { upsertRow } from '../upsertRow'; +import { SQL } from 'drizzle-orm'; import buildQuery from '../queries/buildQuery'; +import { upsertRow } from '../upsertRow'; export const updateOne: UpdateOne = async function updateOne({ collection: collectionSlug, data, + draft, + id, + locale, req, where, - draft, - locale, }) { const collection = this.payload.collections[collectionSlug].config; - const query = await buildQuery({ - adapter: this, - collectionSlug, - locale, - where, - }); + let query: SQL; + + if (where) { + query = await buildQuery({ + adapter: this, + collectionSlug, + locale, + where, + }); + } const result = await upsertRow({ adapter: this, data, fallbackLocale: req.fallbackLocale, fields: collection.fields, + id, locale: req.locale, - operation: 'update', - query, tableName: toSnakeCase(collectionSlug), + where: query, }); return result; diff --git a/packages/db-postgres/src/upsertRow/index.ts b/packages/db-postgres/src/upsertRow/index.ts index e34fc24db9..f320a3a408 100644 --- a/packages/db-postgres/src/upsertRow/index.ts +++ b/packages/db-postgres/src/upsertRow/index.ts @@ -1,11 +1,11 @@ /* eslint-disable no-param-reassign */ import { Field } from 'payload/types'; -import { SQL } from 'drizzle-orm'; -import { PostgresAdapter } from '../types'; -import { traverseFields } from './traverseFields'; -import { transform } from '../transform'; -import { BlockRowToInsert, RowToInsert } from './types'; -import { insertArrays } from './insertArrays'; +import { SQL, and, eq, inArray } from 'drizzle-orm'; +import { GenericColumn, PostgresAdapter } from '../types'; +import { transform } from '../transform/read'; +import { BlockRowToInsert } from '../transform/write/types'; +import { insertArrays } from '../insertArrays'; +import { transformForWrite } from '../transform/write'; type Args = { adapter: PostgresAdapter @@ -13,60 +13,55 @@ type Args = { fallbackLocale?: string | false fields: Field[] locale: string - operation: 'create' | 'update' path?: string - query?: SQL tableName: string -} + upsertTarget?: GenericColumn +} & ({ + where: SQL + id: never +} | { + id: string | number + where: never +}) export const upsertRow = async ({ adapter, data, fallbackLocale, fields, + id, locale, - operation, path = '', - query, tableName, + upsertTarget, + where, }: Args): Promise> => { // Split out the incoming data into the corresponding: // base row, locales, relationships, blocks, and arrays - const rowToInsert: RowToInsert = { - row: {}, - locale: {}, - relationships: [], - blocks: {}, - arrays: {}, - }; - - // This function is responsible for building up the - // above rowToInsert - traverseFields({ - adapter, - arrays: rowToInsert.arrays, - blocks: rowToInsert.blocks, - columnPrefix: '', + const rowToInsert = transformForWrite({ data, fields, locale, - localeRow: rowToInsert.locale, - newTableName: tableName, - parentTableName: tableName, path, - relationships: rowToInsert.relationships, - row: rowToInsert.row, + tableName, }); + const target = upsertTarget || adapter.tables[tableName].id; + + // First, we insert the main row let insertedRow: Record; - // First, we insert / update the main row - if (operation === 'create') { + if (id) { + rowToInsert.row.id = id; [insertedRow] = await adapter.db.insert(adapter.tables[tableName]) - .values(rowToInsert.row).returning(); + .values(rowToInsert.row) + .onConflictDoUpdate({ target, set: rowToInsert.row }) + .returning(); } else { - [insertedRow] = await adapter.db.update(adapter.tables[tableName]) - .set(rowToInsert.row).where(query).returning(); + [insertedRow] = await adapter.db.insert(adapter.tables[tableName]) + .values(rowToInsert.row) + .onConflictDoUpdate({ target, set: rowToInsert.row, where }) + .returning(); } let localeToInsert: Record; @@ -102,28 +97,21 @@ export const upsertRow = async ({ }); }); - if (operation === 'update') { - // TODO: delete existing data for paths that are updated - // 1. Arrays, and all sub-arrays from that point on including all locales - // 2. Blocks by path including locales, and sub arrays - // 3. Relationships by path - } - // ////////////////////////////////// // INSERT LOCALES // ////////////////////////////////// - let insertedLocaleRow; + let insertedLocaleRow: Record; if (localeToInsert) { promises.push(async () => { - if (operation === 'create') { - [insertedLocaleRow] = await adapter.db.insert(adapter.tables[`${tableName}_locales`]) - .values(localeToInsert).returning(); - } else { - // TODO: QUERY for existing locale by parent ID, to get its ID - // and upsert if it doesn't exist already - } + [insertedLocaleRow] = await adapter.db.insert(adapter.tables[`${tableName}_locales`]) + .values(localeToInsert) + .onConflictDoUpdate({ + target: [adapter.tables[`${tableName}_locales`]._locale, adapter.tables[`${tableName}_locales`]._parentID], + set: localeToInsert, + }) + .returning(); }); } @@ -135,6 +123,42 @@ export const upsertRow = async ({ if (relationsToInsert.length > 0) { promises.push(async () => { + // Delete any relationship rows for parent ID and paths that have been updated + // prior to recreating them + const localizedPathsToDelete = new Set(); + const pathsToDelete = new Set(); + + relationsToInsert.forEach((relation) => { + if (typeof relation.path === 'string') { + if (typeof relation.locale === 'string') { + localizedPathsToDelete.add(relation.path); + } else { + pathsToDelete.add(relation.path); + } + } + }); + + if (localizedPathsToDelete.size > 0) { + await adapter.db.delete(adapter.tables[`${tableName}_relationships`]) + .where( + and( + eq(adapter.tables[`${tableName}_relationships`].parent, insertedRow.id), + inArray(adapter.tables[`${tableName}_relationships`].path, [localizedPathsToDelete]), + eq(adapter.tables[`${tableName}_relationships`].locale, locale), + ), + ); + } + + if (pathsToDelete.size > 0) { + await adapter.db.delete(adapter.tables[`${tableName}_relationships`]) + .where( + and( + eq(adapter.tables[`${tableName}_relationships`].parent, insertedRow.id), + inArray(adapter.tables[`${tableName}_relationships`].path, Array.from(pathsToDelete)), + ), + ); + } + insertedRelationshipRows = await adapter.db.insert(adapter.tables[`${tableName}_relationships`]) .values(relationsToInsert).returning(); }); diff --git a/src/auth/operations/resetPassword.ts b/src/auth/operations/resetPassword.ts index 48aa74f20f..6c5375aa2c 100644 --- a/src/auth/operations/resetPassword.ts +++ b/src/auth/operations/resetPassword.ts @@ -82,7 +82,7 @@ async function resetPassword(args: Arguments): Promise { const doc = await payload.db.updateOne({ collection: collectionConfig.slug, - where: { id: { equals: user.id } }, + id: user.id, data: user, req, }); diff --git a/src/auth/operations/verifyEmail.ts b/src/auth/operations/verifyEmail.ts index e32e4dbf1f..110fff5eb6 100644 --- a/src/auth/operations/verifyEmail.ts +++ b/src/auth/operations/verifyEmail.ts @@ -31,7 +31,7 @@ async function verifyEmail(args: Args): Promise { await req.payload.db.updateOne({ collection: collection.config.slug, - where: { id: { equals: user.id } }, + id: user.id, data: { _verified: true, _verificationToken: null, diff --git a/src/collections/operations/restoreVersion.ts b/src/collections/operations/restoreVersion.ts index 82ce7b3a16..33d6bac0ca 100644 --- a/src/collections/operations/restoreVersion.ts +++ b/src/collections/operations/restoreVersion.ts @@ -110,7 +110,7 @@ async function restoreVersion(args: Arguments): Prom let result = await req.payload.db.updateOne({ collection: collectionConfig.slug, - where: { id: { equals: parentDocID } }, + id: parentDocID, data: rawVersion.version, req, }); diff --git a/src/collections/operations/update.ts b/src/collections/operations/update.ts index 5b4bec4177..2e883a1188 100644 --- a/src/collections/operations/update.ts +++ b/src/collections/operations/update.ts @@ -273,7 +273,7 @@ async function update( result = await req.payload.db.updateOne({ collection: collectionConfig.slug, locale, - where: { id: { equals: id } }, + id, data: result, req, }); diff --git a/src/collections/operations/updateByID.ts b/src/collections/operations/updateByID.ts index 4704581124..56e22c241b 100644 --- a/src/collections/operations/updateByID.ts +++ b/src/collections/operations/updateByID.ts @@ -258,7 +258,7 @@ async function updateByID( result = await req.payload.db.updateOne({ collection: collectionConfig.slug, locale, - where: { id: { equals: id } }, + id, data: dataToUpdate, req, }); diff --git a/src/database/types.ts b/src/database/types.ts index ab470341cc..4dacebfc20 100644 --- a/src/database/types.ts +++ b/src/database/types.ts @@ -265,11 +265,16 @@ export type DeleteVersions = (args: DeleteVersionsArgs) => Promise; export type UpdateVersionArgs = { collectionSlug: string - where: Where locale?: string versionData: T req?: PayloadRequest -} +} & ({ + where: Where + id: never +} | { + id: string | number + where: never +}) export type UpdateVersion = (args: UpdateVersionArgs) => Promise> @@ -284,25 +289,19 @@ export type CreateArgs = { export type Create = (args: CreateArgs) => Promise -export type UpdateArgs = { - collection: string - data: Record - where: Where - draft?: boolean - locale?: string - req?: PayloadRequest -} - -export type Update = (args: UpdateArgs) => Promise - export type UpdateOneArgs = { collection: string data: Record - where: Where draft?: boolean locale?: string req?: PayloadRequest -} +} & ({ + where: Where + id: never +} | { + where: never + id: string | number +}) export type UpdateOne = (args: UpdateOneArgs) => Promise diff --git a/src/payload.ts b/src/payload.ts index 1b2dc89fd7..c1fe493fc1 100644 --- a/src/payload.ts +++ b/src/payload.ts @@ -1,8 +1,8 @@ +import crypto from 'crypto'; +import path from 'path'; import pino from 'pino'; import type { Express, Router } from 'express'; import { ExecutionResult, GraphQLSchema, ValidationRule } from 'graphql'; -import crypto from 'crypto'; -import path from 'path'; import { Config as GeneratedTypes } from 'payload/generated-types'; import { OperationArgs, Request as graphQLRequest } from 'graphql-http/lib/handler'; import { SendMailOptions } from 'nodemailer'; diff --git a/src/versions/saveVersion.ts b/src/versions/saveVersion.ts index 83591217a9..86eac089e2 100644 --- a/src/versions/saveVersion.ts +++ b/src/versions/saveVersion.ts @@ -72,11 +72,7 @@ export const saveVersion = async ({ result = await payload.db.updateVersion({ collectionSlug: entityConfig.slug, versionData: data, - where: { - id: { - equals: latestVersion.id, - }, - }, + id: latestVersion.id, req, }); }