@proteinjs/db 1.11.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/generated/index.js +1 -1
  3. package/dist/generated/index.js.map +1 -1
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +1 -0
  7. package/dist/index.js.map +1 -1
  8. package/dist/src/Columns.d.ts +30 -0
  9. package/dist/src/Columns.d.ts.map +1 -1
  10. package/dist/src/Columns.js +203 -1
  11. package/dist/src/Columns.js.map +1 -1
  12. package/dist/src/Db.d.ts +2 -1
  13. package/dist/src/Db.d.ts.map +1 -1
  14. package/dist/src/Db.js +8 -7
  15. package/dist/src/Db.js.map +1 -1
  16. package/dist/src/Record.d.ts +1 -1
  17. package/dist/src/Record.d.ts.map +1 -1
  18. package/dist/src/Record.js +3 -3
  19. package/dist/src/Record.js.map +1 -1
  20. package/dist/src/Table.d.ts +5 -4
  21. package/dist/src/Table.d.ts.map +1 -1
  22. package/dist/src/Table.js +13 -12
  23. package/dist/src/Table.js.map +1 -1
  24. package/dist/src/schema/TableManager.d.ts +1 -0
  25. package/dist/src/schema/TableManager.d.ts.map +1 -1
  26. package/dist/src/schema/TableManager.js +48 -1
  27. package/dist/src/schema/TableManager.js.map +1 -1
  28. package/dist/src/transaction/TransactionRunner.d.ts +5 -0
  29. package/dist/src/transaction/TransactionRunner.d.ts.map +1 -1
  30. package/dist/src/transaction/TransactionRunner.js +5 -0
  31. package/dist/src/transaction/TransactionRunner.js.map +1 -1
  32. package/dist/test/reusable/DynamicReferenceColumn.d.ts +77 -0
  33. package/dist/test/reusable/DynamicReferenceColumn.d.ts.map +1 -0
  34. package/dist/test/reusable/DynamicReferenceColumn.js +656 -0
  35. package/dist/test/reusable/DynamicReferenceColumn.js.map +1 -0
  36. package/generated/index.ts +1 -1
  37. package/index.ts +1 -0
  38. package/package.json +3 -3
  39. package/src/Columns.ts +190 -1
  40. package/src/Db.ts +12 -6
  41. package/src/Record.ts +7 -3
  42. package/src/Table.ts +17 -7
  43. package/src/schema/TableManager.ts +61 -0
  44. package/src/transaction/TransactionRunner.ts +6 -0
  45. package/test/reusable/DynamicReferenceColumn.ts +487 -0
package/src/Columns.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import moment from 'moment';
2
2
  import { v1 as uuidv1 } from 'uuid';
3
- import { Column, ColumnOptions, Table, tableByName } from './Table';
3
+ import { Column, ColumnOptions, getColumnPropertyName, Table, tableByName } from './Table';
4
4
  import { Record } from './Record';
5
5
  import { ReferenceArray } from './reference/ReferenceArray';
6
6
  import { Db } from './Db';
@@ -350,3 +350,192 @@ export class ReferenceColumn<T extends Record> extends StringColumn<Reference<T>
350
350
  await new Db().delete(referenceTable, qb);
351
351
  }
352
352
  }
353
+
354
+ /** Column type for storing table names that links to a DynamicReferenceColumn */
355
+ export class DynamicReferenceTableNameColumn extends StringColumn<string> {
356
+ constructor(
357
+ name: string,
358
+ public referenceColumnName: string,
359
+ options?: ColumnOptions
360
+ ) {
361
+ const enhancedOptions = {
362
+ ...options,
363
+ defaultValue: async (table: Table<any>, record: any) => {
364
+ const colPropertyName = getColumnPropertyName(table, name);
365
+ const refColPropertyName = getColumnPropertyName(table, referenceColumnName);
366
+ if (!colPropertyName) {
367
+ throw new Error(`Column ${name} in table ${table.name} not found when setting default value`);
368
+ }
369
+ if (!refColPropertyName) {
370
+ throw new Error(`Column ${referenceColumnName} in table ${table.name} not found when setting default value`);
371
+ }
372
+
373
+ // No reference is being set, so we can return early
374
+ if (!record[refColPropertyName]) {
375
+ return options?.defaultValue ? await options.defaultValue(table, record) : record[colPropertyName] ?? null;
376
+ }
377
+
378
+ // Get the table name from the reference column
379
+ const { _table: referenceTableName } = record[refColPropertyName];
380
+
381
+ if (!referenceTableName) {
382
+ throw new Error(
383
+ `When inserting, table name must be set in Reference object for DynamicReferenceColumn ${referenceColumnName}`
384
+ );
385
+ }
386
+
387
+ // Assign the table name and return it, unless an defaultValue function is provided via options
388
+ record[colPropertyName] = referenceTableName;
389
+ return options?.defaultValue ? await options.defaultValue(table, record) : referenceTableName;
390
+ },
391
+ updateValue: async (table: Table<any>, updateObj: any) => {
392
+ const colPropertyName = getColumnPropertyName(table, name);
393
+ const refColPropertyName = getColumnPropertyName(table, referenceColumnName);
394
+ if (!colPropertyName) {
395
+ throw new Error(`Column ${name} in table ${table.name} not found when setting default value`);
396
+ }
397
+ if (!refColPropertyName) {
398
+ throw new Error(`Column ${referenceColumnName} in table ${table.name} not found when setting default value`);
399
+ }
400
+
401
+ // The reference column is not being updated, so we can return early
402
+ if (!updateObj[refColPropertyName]) {
403
+ return options?.updateValue?.(table, updateObj) ?? updateObj[colPropertyName] ?? undefined;
404
+ }
405
+
406
+ // Get the table name from the new reference column
407
+ const { _table: newTableName } = updateObj[refColPropertyName];
408
+
409
+ if (!newTableName) {
410
+ throw new Error(
411
+ `When inserting, table name must be set in Reference object for DynamicReferenceColumn ${referenceColumnName}`
412
+ );
413
+ }
414
+
415
+ // Assign the new table name and return it, unless an updateValue function is provided via options
416
+ updateObj[colPropertyName] = newTableName;
417
+ return options?.updateValue?.(table, updateObj) ?? newTableName;
418
+ },
419
+ };
420
+
421
+ super(
422
+ name,
423
+ Object.assign(
424
+ {
425
+ ui: {
426
+ hidden: true,
427
+ },
428
+ },
429
+ enhancedOptions
430
+ )
431
+ );
432
+ }
433
+ }
434
+
435
+ /**
436
+ * Creates a dynamic reference column that can link to records in any table
437
+ *
438
+ * The reference is stored as two columns:
439
+ * 1. A `DynamicReferenceTableNameColumn` storing the reference's table name, which is managed internally and should not be set or updated
440
+ * 2. A `DynamicReferenceColumn` which is a reference to a record
441
+ *
442
+ * @example
443
+ * {
444
+ * referenceTableName: new DynamicReferenceTableNameColumn('reference_table_name', 'dynamic_reference'),
445
+ * dynamicReference: new DynamicReferenceColumn<EntityType>(
446
+ * 'dynamic_reference',
447
+ * 'reference_table_name', // Name of column containing table name
448
+ * )
449
+ * }
450
+ */
451
+
452
+ export class DynamicReferenceColumn<T extends Record> extends StringColumn<Reference<T>> {
453
+ constructor(
454
+ name: string,
455
+ public dynamicRefTableColName: string,
456
+ public cascadeDelete: boolean = false,
457
+ options?: ColumnOptions
458
+ ) {
459
+ super(
460
+ name,
461
+ Object.assign(
462
+ {
463
+ ui: {
464
+ hidden: true,
465
+ },
466
+ },
467
+ options
468
+ ),
469
+ 36
470
+ );
471
+ }
472
+
473
+ async serialize(fieldValue: Reference<T> | null | undefined): Promise<string | null> {
474
+ if (fieldValue === undefined || fieldValue == null || !fieldValue._id) {
475
+ return null;
476
+ }
477
+
478
+ if (!fieldValue._table || fieldValue._table.trim() === '') {
479
+ throw new Error(`Table name must be provided for DynamicReferenceColumn ${this.name}`);
480
+ }
481
+
482
+ return fieldValue._id;
483
+ }
484
+
485
+ async deserialize(serializedFieldValue: string, serializedRecord: any): Promise<Reference<T> | null> {
486
+ const reference = new Reference('', serializedFieldValue);
487
+ if (reference._id === null) {
488
+ return null;
489
+ }
490
+
491
+ const tableName = serializedRecord[this.dynamicRefTableColName];
492
+ if (!tableName) {
493
+ throw new Error(`Table name not found in column ${this.dynamicRefTableColName} for reference ${this.name}`);
494
+ }
495
+
496
+ return new Reference<T>(tableName, serializedFieldValue);
497
+ }
498
+
499
+ async beforeDelete(
500
+ table: Table<any>,
501
+ columnPropertyName: string,
502
+ records: any[],
503
+ getTable?: (tableName: string) => Table<any>,
504
+ db?: Db
505
+ ): Promise<void> {
506
+ if (!this.cascadeDelete) {
507
+ return;
508
+ }
509
+
510
+ const getTableFn = getTable ? getTable : tableByName;
511
+ const dbInstance = db ? db : new Db();
512
+
513
+ // Get all referenced record IDs grouped by table
514
+ const recordsToDelete = new Map<string, string[]>();
515
+
516
+ for (const record of records) {
517
+ const reference = record[columnPropertyName] as Reference<Record>;
518
+ if (!reference?._id || !reference._table) {
519
+ continue;
520
+ }
521
+
522
+ if (!recordsToDelete.has(reference._table)) {
523
+ recordsToDelete.set(reference._table, []);
524
+ }
525
+ recordsToDelete.get(reference._table)!.push(reference._id);
526
+ }
527
+
528
+ // Delete records from each referenced table using Promise.all to properly await all deletions
529
+ const entries = Array.from(recordsToDelete.entries());
530
+ console.log(`entries to delete: ${JSON.stringify(entries)}`);
531
+ for (const [tableName, ids] of entries) {
532
+ if (ids.length > 0) {
533
+ const referenceTable = getTableFn(tableName);
534
+ const qb = new QueryBuilderFactory()
535
+ .getQueryBuilder(referenceTable)
536
+ .condition({ field: 'id', operator: 'IN', value: ids });
537
+ await dbInstance.delete(referenceTable, qb);
538
+ }
539
+ }
540
+ }
541
+ }
package/src/Db.ts CHANGED
@@ -75,7 +75,7 @@ export class Db<R extends Record = Record> implements DbService<R> {
75
75
 
76
76
  constructor(
77
77
  dbDriver?: DbDriver,
78
- getTable?: (tableName: string) => Table<any>,
78
+ private getTable?: (tableName: string) => Table<any>,
79
79
  transactionContextFactory?: DefaultTransactionContextFactory,
80
80
  private runAsSystem: boolean = false
81
81
  ) {
@@ -169,8 +169,8 @@ export class Db<R extends Record = Record> implements DbService<R> {
169
169
  }
170
170
 
171
171
  recordCopy = await this.tableWatcherRunner.runBeforeUpdateTableWatchers(table, recordCopy, qb);
172
- const recordSearializer = new RecordSerializer<T>(table);
173
- const serializedRecord = await recordSearializer.serialize(recordCopy);
172
+ const recordSerializer = new RecordSerializer<T>(table);
173
+ const serializedRecord = await recordSerializer.serialize(recordCopy);
174
174
  delete serializedRecord['id'];
175
175
  const generateUpdate = (config: DbDriverDmlStatementConfig) =>
176
176
  new StatementFactory<T>().update(
@@ -212,7 +212,13 @@ export class Db<R extends Record = Record> implements DbService<R> {
212
212
  for (const columnPropertyName in table.columns) {
213
213
  const column = (table.columns as any)[columnPropertyName] as Column<any, any>;
214
214
  if (typeof column.beforeDelete !== 'undefined') {
215
- await column.beforeDelete(table, columnPropertyName, recordsToDelete);
215
+ await column.beforeDelete(
216
+ table,
217
+ columnPropertyName,
218
+ recordsToDelete,
219
+ this.getTable,
220
+ new Db(this.dbDriver, this.getTable, this.transactionContextFactory)
221
+ );
216
222
  }
217
223
  }
218
224
  }
@@ -257,9 +263,9 @@ export class Db<R extends Record = Record> implements DbService<R> {
257
263
  const generateQuery = (config: DbDriverQueryStatementConfig) =>
258
264
  qb.toSql(this.statementConfigFactory.getStatementConfig(config));
259
265
  const serializedRecords = await this.dbDriver.runQuery(generateQuery, this.currentTransaction);
260
- const recordSearializer = new RecordSerializer(table);
266
+ const recordSerializer = new RecordSerializer(table);
261
267
  return await Promise.all(
262
- serializedRecords.map(async (serializedRecord) => recordSearializer.deserialize(serializedRecord))
268
+ serializedRecords.map(async (serializedRecord) => recordSerializer.deserialize(serializedRecord))
263
269
  );
264
270
  }
265
271
 
package/src/Record.ts CHANGED
@@ -87,7 +87,11 @@ export class RecordSerializer<T extends Record> {
87
87
  for (const columnName in serializedRecord) {
88
88
  const serializedFieldValue = serializedRecord[columnName];
89
89
  try {
90
- const { fieldPropertyName, fieldValue } = await fieldSerializer.deserialize(columnName, serializedFieldValue);
90
+ const { fieldPropertyName, fieldValue } = await fieldSerializer.deserialize(
91
+ columnName,
92
+ serializedFieldValue,
93
+ serializedRecord
94
+ );
91
95
  deserialized[fieldPropertyName] = fieldValue;
92
96
  } catch (MissingFieldError) {
93
97
  omittedFields.push(columnName);
@@ -121,7 +125,7 @@ export class FieldSerializer<T extends Record> {
121
125
  return { columnName: column.name, serializedFieldValue };
122
126
  }
123
127
 
124
- async deserialize(columnName: string, serializedFieldValue: any) {
128
+ async deserialize(columnName: string, serializedFieldValue: any, serializedRecord: SerializedRecord) {
125
129
  const columns: { [prop: string]: Column<any, any> } = this.table.columns;
126
130
  let fieldPropertyName = columnName;
127
131
  let column = columns[columnName]; // the scenario that the column name is the same as the property name
@@ -143,7 +147,7 @@ export class FieldSerializer<T extends Record> {
143
147
 
144
148
  let fieldValue = serializedFieldValue;
145
149
  if (column.deserialize) {
146
- fieldValue = await column.deserialize(serializedFieldValue);
150
+ fieldValue = await column.deserialize(serializedFieldValue, serializedRecord);
147
151
  }
148
152
 
149
153
  return { fieldPropertyName, fieldValue };
package/src/Table.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import { Loadable, SourceRepository } from '@proteinjs/reflection';
2
2
  import { CustomSerializableObject } from '@proteinjs/serializer';
3
- import { Record } from './Record';
3
+ import { Record, RecordSerializer } from './Record';
4
4
  import { TableSerializerId } from './serializers/TableSerializer';
5
5
  import { QueryBuilder } from '@proteinjs/db-query';
6
6
  import { Identity, TableOperationsAuth } from './auth/TableAuth';
7
+ import { Db } from './Db';
7
8
 
8
9
  export const isTable = (obj: any) => obj.__serializerId === TableSerializerId;
9
10
 
@@ -51,7 +52,7 @@ export const addDefaultFieldValues = async (table: Table<any>, record: any, runA
51
52
  column.options?.forceDefaultValue === true ||
52
53
  (typeof column.options?.forceDefaultValue === 'function' && column.options.forceDefaultValue(runAsSystem)))
53
54
  ) {
54
- record[columnPropertyName] = await column.options.defaultValue(record);
55
+ record[columnPropertyName] = await column.options.defaultValue(table, record);
55
56
  }
56
57
  }
57
58
  };
@@ -60,7 +61,10 @@ export const addUpdateFieldValues = async (table: Table<any>, record: any) => {
60
61
  for (const columnPropertyName in table.columns) {
61
62
  const column = (table.columns as any)[columnPropertyName] as Column<any, any>;
62
63
  if (column.options?.updateValue) {
63
- record[columnPropertyName] = await column.options.updateValue(record);
64
+ const value = await column.options.updateValue(table, record);
65
+ if (value !== undefined) {
66
+ record[columnPropertyName] = value;
67
+ }
64
68
  }
65
69
  }
66
70
  };
@@ -120,8 +124,14 @@ export type Column<T, Serialized> = {
120
124
  oldName?: string;
121
125
  options?: ColumnOptions;
122
126
  serialize?: (fieldValue: T | null | undefined) => Promise<Serialized | null | undefined>;
123
- deserialize?: (serializedFieldValue: Serialized | null) => Promise<T | null | void>;
124
- beforeDelete?: (table: Table<any>, columnPropertyName: string, records: any[]) => Promise<void>;
127
+ deserialize?: (serializedFieldValue: Serialized | null, serializedRecord: any) => Promise<T | null | void>;
128
+ beforeDelete?: (
129
+ table: Table<any>,
130
+ columnPropertyName: string,
131
+ records: any[],
132
+ getTable?: (tableName: string) => Table<any>,
133
+ db?: Db
134
+ ) => Promise<void>;
125
135
  };
126
136
 
127
137
  export type ColumnOptions = {
@@ -134,11 +144,11 @@ export type ColumnOptions = {
134
144
  references?: { table: string };
135
145
  nullable?: boolean;
136
146
  /** Value stored on insert */
137
- defaultValue?: (insertObj: any) => Promise<any>;
147
+ defaultValue?: (table: Table<any>, insertObj: any) => Promise<any>;
138
148
  /** If true, the `defaultValue` function will always provide the value and override any existing value */
139
149
  forceDefaultValue?: boolean | ((runAsSystem: boolean) => boolean);
140
150
  /** Value stored on update */
141
- updateValue?: (updateObj: any) => Promise<any>;
151
+ updateValue?: (table: Table<any>, updateObj: any) => Promise<any>;
142
152
  /** Add conditions to query; called on every query of this table */
143
153
  addToQuery?: (qb: QueryBuilder, runAsSystem: boolean) => Promise<void>;
144
154
  ui?: {
@@ -3,6 +3,7 @@ import { Column, Table, getTables } from '../Table';
3
3
  import { SchemaOperations, TableChanges } from './SchemaOperations';
4
4
  import { SchemaMetadata } from './SchemaMetadata';
5
5
  import { DbDriver } from '../Db';
6
+ import { DynamicReferenceColumn, DynamicReferenceTableNameColumn } from '../Columns';
6
7
 
7
8
  export interface ColumnTypeFactory {
8
9
  getType(column: Column<any, any>): string;
@@ -37,6 +38,8 @@ export class TableManager {
37
38
  }
38
39
 
39
40
  async loadTable(table: Table<any>): Promise<void> {
41
+ this.validateDynamicReferenceColumns(table);
42
+
40
43
  if (await this.tableExists(table)) {
41
44
  const tableChanges = await this.getTableChanges(table);
42
45
  if (this.shouldAlterTable(tableChanges)) {
@@ -51,6 +54,64 @@ export class TableManager {
51
54
  }
52
55
  }
53
56
 
57
+ private validateDynamicReferenceColumns(table: Table<any>): void {
58
+ const isDynamicRefColumn = (column: any): column is DynamicReferenceColumn<any> =>
59
+ typeof column.dynamicRefTableColName === 'string';
60
+
61
+ const isDynamicRefTableNameColumn = (column: any): column is DynamicReferenceTableNameColumn =>
62
+ typeof column.referenceColumnName === 'string';
63
+
64
+ // Quick check if there are any dynamic reference columns
65
+ const hasDynamicColumns = Object.values(table.columns).some(
66
+ (column) => isDynamicRefColumn(column) || isDynamicRefTableNameColumn(column)
67
+ );
68
+
69
+ if (!hasDynamicColumns) {
70
+ return;
71
+ }
72
+
73
+ interface DynamicRefColumnInfo {
74
+ columnName: string;
75
+ tableColumnName: string;
76
+ }
77
+
78
+ const dynamicRefColumns: DynamicRefColumnInfo[] = [];
79
+ const dynamicRefTableNameColumns: string[] = [];
80
+
81
+ // First pass: collect all dynamic reference columns
82
+ Object.entries(table.columns).forEach(([propertyName, column]) => {
83
+ if (isDynamicRefColumn(column)) {
84
+ dynamicRefColumns.push({
85
+ columnName: column.name,
86
+ tableColumnName: column.dynamicRefTableColName,
87
+ });
88
+ } else if (isDynamicRefTableNameColumn(column)) {
89
+ dynamicRefTableNameColumns.push(column.name);
90
+ }
91
+ });
92
+
93
+ // Second pass: validate that each DynamicReferenceColumn has its required table name column
94
+ dynamicRefColumns.forEach(({ columnName, tableColumnName }) => {
95
+ if (!dynamicRefTableNameColumns.includes(tableColumnName)) {
96
+ throw new Error(
97
+ `Table ${table.name} has a DynamicReferenceColumn '${columnName}' but is missing its required DynamicReferenceTableNameColumn '${tableColumnName}'`
98
+ );
99
+ }
100
+ });
101
+
102
+ // Third pass: validate that each DynamicReferenceTableNameColumn is used by a DynamicReferenceColumn
103
+ dynamicRefTableNameColumns.forEach((tableNameColumnName) => {
104
+ const hasMatchingRefColumn = dynamicRefColumns.some(
105
+ ({ tableColumnName }) => tableColumnName === tableNameColumnName
106
+ );
107
+ if (!hasMatchingRefColumn) {
108
+ throw new Error(
109
+ `Table ${table.name} has a DynamicReferenceTableNameColumn '${tableNameColumnName}' but no DynamicReferenceColumn references it`
110
+ );
111
+ }
112
+ });
113
+ }
114
+
54
115
  private shouldAlterTable(tableChanges: TableChanges) {
55
116
  if (
56
117
  tableChanges.columnsToCreate.length == 0 &&
@@ -6,6 +6,12 @@ export const getTransactionRunner = () =>
6
6
  typeof self === 'undefined' ? new TransactionRunner() : (getTransactionRunnerService() as TransactionRunner);
7
7
 
8
8
  export class TransactionRunner implements TransactionRunnerService {
9
+ public serviceMetadata = {
10
+ auth: {
11
+ allUsers: true,
12
+ },
13
+ };
14
+
9
15
  async run(ops: Operation<any>[]): Promise<void> {
10
16
  const db = getDb();
11
17