diff --git a/packages/data/src/schema/mutate.ts b/packages/data/src/schema/mutate.ts index a4684c8..03b0721 100644 --- a/packages/data/src/schema/mutate.ts +++ b/packages/data/src/schema/mutate.ts @@ -14,7 +14,7 @@ import { table as entity } from './entity/entity.ts'; /** * Possible operations to a historical change. */ -export const mutateOperation = pgEnum('mutate_operation', [ 'create', 'update', 'delete' ]); +export const mutateOperation = pgEnum('mutate_operation', [ 'insert', 'update', 'remove' ]); /** * Defines a log of CUD changes to records on other tables in the database. diff --git a/packages/data/src/transaction/audit.ts b/packages/data/src/transaction/audit.ts index dcbb3e6..2894361 100644 --- a/packages/data/src/transaction/audit.ts +++ b/packages/data/src/transaction/audit.ts @@ -4,7 +4,7 @@ import { getTableName, Table, type TableConfig } from 'drizzle-orm'; import { schema } from '@do-ob/data/schema'; export interface AuditMutationChanges { - type: 'create' | 'update' | 'delete'; + type: 'insert' | 'update' | 'remove'; table: Table, value: { $id: string, [key: string]: unknown }, } diff --git a/packages/data/src/transaction/insert.ts b/packages/data/src/transaction/insert.ts index 0f26e23..89b82eb 100644 --- a/packages/data/src/transaction/insert.ts +++ b/packages/data/src/transaction/insert.ts @@ -41,12 +41,12 @@ export function insert< if ($dispatch) { await tx.transaction(auditMutation($dispatch, [ { - type: 'create', + type: 'insert', table, value: typeRecord, }, { - type: 'create', + type: 'insert', table: schema.entity, value: entityRecord, } diff --git a/packages/data/src/transaction/remove.test.ts b/packages/data/src/transaction/remove.test.ts new file mode 100644 index 0000000..703a01d --- /dev/null +++ b/packages/data/src/transaction/remove.test.ts @@ -0,0 +1,25 @@ +import { test, expect, beforeAll } from 'vitest'; +import { Database, database } from '@do-ob/data/database'; +import { seed } from '@do-ob/data/seed'; +import { schema } from '@do-ob/data/schema'; +import { prepareInput } from '@/test/utility'; +import { remove } from './remove'; + +let db: Database; + +beforeAll(async () => { + db = await seed(database()); +}); + +test('remove an entity', async () => { + const input = await prepareInput(db); + const result = await db.transaction( + remove(input, schema.entity, '00000000-0000-0000-0000-000000000000'), + ); + + expect(result).toEqual([ + { + $id: '00000000-0000-0000-0000-000000000000', + }, + ]); +}); diff --git a/packages/data/src/transaction/remove.ts b/packages/data/src/transaction/remove.ts new file mode 100644 index 0000000..5253a6e --- /dev/null +++ b/packages/data/src/transaction/remove.ts @@ -0,0 +1,95 @@ +import type { Transaction } from './transaction.types'; +import { and, eq, SQL, sql, type TableConfig } from 'drizzle-orm'; +import type { PgTableWithColumns } from 'drizzle-orm/pg-core'; +import { schema } from '@do-ob/data/schema'; +import { auditMutation } from './audit'; +import { Ambit, type Input } from '@do-ob/core'; +import { RowList } from 'postgres'; + +/** + * Builds an sql filter based on an ambit. + */ +function scope( + $subject: string, + ambit: Ambit, +): SQL { + switch(ambit) { + case Ambit.Global: + return sql`true`; + case Ambit.Owned: + return eq(schema.entity.$owner, $subject); + case Ambit.Created: + return eq(schema.entity.$creator, $subject); + case Ambit.Member: + return sql`false`; // TODO: Implement member scope. + case Ambit.None: + default: + return sql`false`; + } +} + +export function remove< + C extends TableConfig, +> ( + input: Input, + table: PgTableWithColumns, + $id: string, +) { + return async (tx: Transaction): Promise<[PgTableWithColumns['$inferSelect']]> => { + const { ambit, $subject, $dispatch } = input; + + if (!$subject) { + throw new Error('Unauthorized. No subject provided for the update operation.'); + } + + /** + * Drizzle ORM does not have the `from` clause for the update builder... + * so we have to build the SQL until then. + * + * See issue for updates: https://github.com/drizzle-team/drizzle-orm/issues/2304 + */ + const chunks: SQL[] = []; + chunks.push(tx.update(schema.entity).set({ deleted: true }).getSQL()); + chunks.push(sql`where ${and( + eq(schema.entity.$id, $id), + scope($subject, ambit), + )}`); + chunks.push(sql`returning *`); + const removeSql = sql.join(chunks, sql.raw(' ')); + + const { rows } = (await tx.execute(removeSql)) as unknown as { rows: RowList }; + + /** + * Rollback if the update failed or it updated more than one record somehow. + */ + if(rows.length === 0 || rows.length > 1) { + tx.rollback(); + } + + /** + * Remove this select statement once the `from` method is available in drizzle-orm. + */ + const [ result ] = await tx.select().from(table).where(eq(table.$id, $id)) as [PgTableWithColumns['$inferSelect'] & { $id: string }]; + + /** + * Rollback if the updated record failed to return anything. + */ + if(!result) { + tx.rollback(); + } + + if ($dispatch) { + await tx.transaction(auditMutation($dispatch, [ + { + type: 'remove', + table, + value: result, + }, + ])); + } + + return [ + result, + ]; + }; +} diff --git a/packages/data/src/transaction/update.ts b/packages/data/src/transaction/update.ts index e3af986..bfb6fc1 100644 --- a/packages/data/src/transaction/update.ts +++ b/packages/data/src/transaction/update.ts @@ -1,5 +1,5 @@ import type { Transaction } from './transaction.types'; -import { and, eq, getTableColumns, SQL, sql, type TableConfig } from 'drizzle-orm'; +import { and, eq, SQL, sql, type TableConfig } from 'drizzle-orm'; import type { PgTableWithColumns } from 'drizzle-orm/pg-core'; import { schema } from '@do-ob/data/schema'; import { auditMutation } from './audit'; @@ -34,6 +34,11 @@ export function update< input: Input, table: PgTableWithColumns, value: Partial['$inferSelect']> & { $id: string }, + + /** + * If true, deleted records will be included in the result. + */ + clairvoyance: boolean = false, ) { return async (tx: Transaction): Promise<[PgTableWithColumns['$inferSelect']]> => { const { $id, ...next } = value; @@ -44,25 +49,22 @@ export function update< } /** - * Drizzle ORM does not have the from method for the update builder... - * so we have to build the SQL manually until then. + * Drizzle ORM does not have the `from` clause for the update builder... + * so we have to build the SQL until then. * * See issue for updates: https://github.com/drizzle-team/drizzle-orm/issues/2304 */ - const updateChunks: SQL[] = []; - updateChunks.push(tx.update(table).set(next as object).getSQL()); - updateChunks.push(sql`from ${schema.entity}`); - updateChunks.push(sql`where ${and( + const chunks: SQL[] = []; + chunks.push(tx.update(table).set(next as object).getSQL()); + chunks.push(sql`from ${schema.entity}`); + chunks.push(sql`where ${and( eq(table.$id, $id), eq(table.$id, schema.entity.$id), + eq(table.deleted, clairvoyance), scope($subject, ambit), )}`); - updateChunks.push(sql`returning ${ - sql.join( - Object.keys(getTableColumns(table)).map((column) => table[column]), sql.raw(', ') - ) - }`); - const updateSql = sql.join(updateChunks, sql.raw(' ')); + chunks.push(sql`returning *`); + const updateSql = sql.join(chunks, sql.raw(' ')); const { rows } = (await tx.execute(updateSql)) as unknown as { rows: RowList };