@proteinjs/db 1.39.0 → 1.41.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proteinjs/db",
3
- "version": "1.39.0",
3
+ "version": "1.41.0",
4
4
  "main": "./dist/generated/index.js",
5
5
  "types": "./dist/generated/index.d.ts",
6
6
  "exports": {
@@ -41,7 +41,7 @@
41
41
  "test": "jest --passWithNoTests"
42
42
  },
43
43
  "dependencies": {
44
- "@proteinjs/db-query": "^1.9.0",
44
+ "@proteinjs/db-query": "^1.10.0",
45
45
  "@proteinjs/logger": "^1.0.21",
46
46
  "@proteinjs/reflection": "^1.2.0",
47
47
  "@proteinjs/serializer": "^1.1.10",
@@ -66,5 +66,5 @@
66
66
  "ts-jest": "29.1.1",
67
67
  "typescript": "5.2.2"
68
68
  },
69
- "gitHead": "bc027111370f4391426dce2e8faf0b862664b3c5"
69
+ "gitHead": "149e538c8a05858b6eb7d3a2dc362e5ff196caf9"
70
70
  }
package/src/Db.ts CHANGED
@@ -59,7 +59,7 @@ export type DbDriverQueryStatementConfig = ParameterizationConfig & {
59
59
  getDriverColumnType?: (tableName: string, columnName: string) => string;
60
60
  handleCaseSensitivity: (tableName: string, columnName: string, caseSensitive: boolean) => string;
61
61
  /** Driver SQL for truncating a datetime column to a bucket unit (`QueryBuilder.timeBucket`). */
62
- dateTruncExpression?: (resolvedColumnName: string, unit: 'day' | 'hour') => string;
62
+ dateTruncExpression?: (resolvedColumnName: string, unit: 'day' | 'hour' | 'minute') => string;
63
63
  };
64
64
 
65
65
  export type DbDriverDmlStatementConfig = ParameterizationConfig & {
package/src/Table.ts CHANGED
@@ -115,6 +115,25 @@ export abstract class Table<T extends Record> implements Loadable, CustomSeriali
115
115
  public cascadeDeleteReferences: () => { table: string; referenceColumn: string }[] = () => [];
116
116
  /** Options for configuring SourceRecords (see {@link SourceRecordOptions}) */
117
117
  public sourceRecordOptions: SourceRecordOptions<T> = {};
118
+ /**
119
+ * Presentation the table declares for the GENERIC record surfaces (@proteinjs/db-ui) — the
120
+ * framework renders what tables declare; per-table display choices never hard-code into the
121
+ * generic components. Distinct from `auth.ui`, which gates WHO may see those surfaces.
122
+ */
123
+ public ui?: {
124
+ recordTable?: {
125
+ /**
126
+ * The columns rendered as the record table's row columns, in this order. `created` and
127
+ * `updated` are appended automatically (the record family's shared face) unless already
128
+ * listed. Undeclared tables get the meaningful-data default pick
129
+ * (db-ui `defaultRecordTableColumns`). Declare when the default pick misses a column a
130
+ * human scans for (e.g. the migration ledger's `duration`) or surfaces one that has no
131
+ * business in a row scan (e.g. an invite's redeemable `token` — row hygiene; the record
132
+ * form still carries the full record).
133
+ */
134
+ columns?: (keyof T & string)[];
135
+ };
136
+ };
118
137
  public auth?: {
119
138
  db?: TableOperationsAuth;
120
139
  service?: TableOperationsAuth;
@@ -45,6 +45,28 @@ export class TableAuthError extends Error {
45
45
  * Util to check which table operations a user can perform
46
46
  */
47
47
  export class TableAuth {
48
+ /**
49
+ * Non-throwing capability read: whether the CURRENT user clears this operation's declared
50
+ * door on this api. The seam UI derivation rides (db-ui's record-table affordances render
51
+ * only the operations the declaration actually opens — a table with no insert door draws no
52
+ * create button); the throwing guards below stay the enforcement path.
53
+ *
54
+ * Answers false — never throws — when the auth machinery itself cannot resolve a user (e.g.
55
+ * a context whose source graph never loaded `AuthenticatedUserRepo`): the same fail-closed
56
+ * reading UserAuth documents for a missing repo, applied to a capability READ.
57
+ */
58
+ canPerform(
59
+ table: Table<any>,
60
+ operation: 'query' | 'insert' | 'update' | 'delete',
61
+ api: 'db' | 'service' = 'db'
62
+ ): boolean {
63
+ try {
64
+ return this.canAccess(table, api, operation);
65
+ } catch {
66
+ return false;
67
+ }
68
+ }
69
+
48
70
  private canAccess(
49
71
  table: Table<any>,
50
72
  api: 'db' | 'service',
@@ -58,14 +58,28 @@ export interface Migration extends SourceRecord {
58
58
  export class MigrationTable extends Table<Migration> {
59
59
  public name = 'migration';
60
60
  /**
61
- * Both doors ride the 'dev' permission (consumer-mapped; admin passes as break-glass),
61
+ * The doors ride the 'dev' permission (consumer-mapped; admin passes as break-glass),
62
62
  * matching MigrationRunner's serviceMetadata: the Migrations record table reads/edits via
63
63
  * the service api, and the runner writes run state via the db api as the calling user.
64
- * Boot-time source-record loading is a system path (getDbAsSystem) and bypasses doors.
64
+ * INSERT deliberately has no door — for anyone, break-glass included: ledger rows are born
65
+ * from source declarations only (boot-time loading rides getDbAsSystem, which bypasses
66
+ * doors), so a caller-path insert could only mint a row no source owns. The generic record
67
+ * surfaces derive their affordances from these doors (db-ui renders no create button here).
65
68
  */
66
69
  public auth: Table<Migration>['auth'] = {
67
- db: { all: { permission: 'dev' } },
68
- service: { all: { permission: 'dev' } },
70
+ db: { query: { permission: 'dev' }, update: { permission: 'dev' }, delete: { permission: 'dev' } },
71
+ service: { query: { permission: 'dev' }, update: { permission: 'dev' }, delete: { permission: 'dev' } },
72
+ };
73
+ /**
74
+ * The row scan the founder actually reads: what ran, how it went, HOW LONG it took
75
+ * (duration — stamped by the runner at completion; the default pick's five-column cap
76
+ * dropped it), then the declaration flags. Failure detail and timestamps stay on the
77
+ * record form.
78
+ */
79
+ public ui: Table<Migration>['ui'] = {
80
+ recordTable: {
81
+ columns: ['description', 'status', 'duration', 'manual', 'preSchemaSync', 'retired'],
82
+ },
69
83
  };
70
84
  public columns = withSourceRecordColumns<Migration>({
71
85
  description: new StringColumn('description', {}, 4000),
@@ -84,4 +84,29 @@ describe('migrations ride the dev permission', () => {
84
84
  expect(() => auth.canUpdate(table, api)).not.toThrow();
85
85
  }
86
86
  });
87
+
88
+ it('insert has no door — for anyone, break-glass included: ledger rows are born from source declarations only', () => {
89
+ const auth = new TableAuth();
90
+ const table = new MigrationTable();
91
+ for (const roles of [['dev-crew'], ['admin']]) {
92
+ setUser(roles);
93
+ for (const api of ['db', 'service'] as const) {
94
+ expect(() => auth.canInsert(table, api)).toThrow(
95
+ 'User is not authorized to insert records into table: migration'
96
+ );
97
+ // The capability read the record surfaces derive affordances from agrees with the gate.
98
+ expect(auth.canPerform(table, 'insert', api)).toBe(false);
99
+ }
100
+ }
101
+ });
102
+
103
+ it('delete keeps its dev door (ledger hygiene stays possible)', () => {
104
+ setUser(['dev-crew']);
105
+ const auth = new TableAuth();
106
+ const table = new MigrationTable();
107
+ for (const api of ['db', 'service'] as const) {
108
+ expect(() => auth.canDelete(table, api)).not.toThrow();
109
+ expect(auth.canPerform(table, 'delete', api)).toBe(true);
110
+ }
111
+ });
87
112
  });