nucleus-core-ts 0.9.855 → 0.9.856

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.
@@ -141,6 +141,34 @@ export function ReferenceCheckRows({ rows, targetFields, sourceFields = [], targ
141
141
  })
142
142
  ]
143
143
  }),
144
+ /*#__PURE__*/ _jsxs("label", {
145
+ className: theme.field.wrapper,
146
+ children: [
147
+ /*#__PURE__*/ _jsx("span", {
148
+ className: theme.field.label,
149
+ children: "Write the matched id to"
150
+ }),
151
+ /*#__PURE__*/ _jsxs("select", {
152
+ className: theme.field.input,
153
+ disabled: disabled,
154
+ id: `${uid}-${index}-write-id`,
155
+ onChange: (event)=>patch(index, {
156
+ writeIdTo: event.target.value || null
157
+ }),
158
+ value: row.writeIdTo ?? '',
159
+ children: [
160
+ /*#__PURE__*/ _jsx("option", {
161
+ value: "",
162
+ children: "Only check that it exists"
163
+ }),
164
+ (targetFields ?? []).map((field)=>/*#__PURE__*/ _jsx("option", {
165
+ value: field,
166
+ children: field
167
+ }, field))
168
+ ]
169
+ })
170
+ ]
171
+ }),
144
172
  /*#__PURE__*/ _jsxs("label", {
145
173
  className: theme.field.wrapper,
146
174
  children: [
@@ -113,6 +113,15 @@ export type ReferenceCheckValue = {
113
113
  /** Column in that entity to match against. */
114
114
  entityColumn: string;
115
115
  onMissing: 'create' | 'skipRecord' | 'ignore';
116
+ /**
117
+ * Column that receives the MATCHED ROW'S ID instead of its value.
118
+ *
119
+ * The engine has done this since references learned to resolve, and nothing
120
+ * in the panel could ask for it — so a product whose link is a uuid got the
121
+ * text column filled and the uuid column left null, and no screen that joins
122
+ * on the id found anything.
123
+ */
124
+ writeIdTo?: string | null;
116
125
  /**
117
126
  * Other columns to fill when this run CREATES the missing value.
118
127
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nucleus-core-ts",
3
- "version": "0.9.855",
3
+ "version": "0.9.856",
4
4
  "description": "Production-ready, enterprise-grade TypeScript framework for building multi-tenant APIs",
5
5
  "author": "Hidayet Can Özcan <hidayetcan@gmail.com>",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'bun:test'
2
- import { generateColumnCode, selectEmittedTables } from './generate-schema'
2
+ import { generateColumnCode, generateTableCode, selectEmittedTables } from './generate-schema'
3
3
 
4
4
  const table = (table_name: string) => ({ table_name })
5
5
 
@@ -85,8 +85,7 @@ describe('selectEmittedTables', () => {
85
85
  * typecheck did not cover the folder it landed in.
86
86
  */
87
87
  describe('numeric defaults are quoted', () => {
88
- const emit = (col: Record<string, unknown>) =>
89
- generateColumnCode(col as never, [], 'widgets')
88
+ const emit = (col: Record<string, unknown>) => generateColumnCode(col as never, [], 'widgets')
90
89
 
91
90
  it('quotes a numeric default, because the column reads as a string', () => {
92
91
  expect(emit({ name: 'position_x', type: 'numeric', notNull: true, default: 0 })).toContain(
@@ -126,7 +125,9 @@ describe('jsonb defaults keep their own shape', () => {
126
125
  it('a REAL array column gets array syntax, not JSON', () => {
127
126
  // `[]` is not array syntax; Postgres refuses the statement outright, and it
128
127
  // took the other sixty in the same batch with it.
129
- expect(emit({ name: 'visible_roles', type: 'text', array: true, default: [] })).toContain("'{}'")
128
+ expect(emit({ name: 'visible_roles', type: 'text', array: true, default: [] })).toContain(
129
+ "'{}'"
130
+ )
130
131
  })
131
132
 
132
133
  it('writes the members of a non-empty array default', () => {
@@ -136,9 +137,7 @@ describe('jsonb defaults keep their own shape', () => {
136
137
  })
137
138
 
138
139
  it('quotes an array member that needs it', () => {
139
- expect(emit({ name: 'roles', type: 'text', array: true, default: ['a b'] })).toContain(
140
- '"a b"'
141
- )
140
+ expect(emit({ name: 'roles', type: 'text', array: true, default: ['a b'] })).toContain('"a b"')
142
141
  })
143
142
 
144
143
  it('an empty object default stays an object', () => {
@@ -153,3 +152,65 @@ describe('jsonb defaults keep their own shape', () => {
153
152
  expect(emit({ name: 'labels', type: 'jsonb', default: ["it's"] })).toContain("''s")
154
153
  })
155
154
  })
155
+
156
+ /**
157
+ * `constraints` has been in the config shape since the beginning and nothing
158
+ * ever read it. Twelve tables in the shipped configs declare a composite unique
159
+ * key; none of them existed in any database. That is why a user could hold the
160
+ * same role twice, a reader could react to one article twice instead of
161
+ * changing their reaction, and two integration connections could be created
162
+ * with the identical slug — which was then matched by an import that wrote to
163
+ * whichever it found first.
164
+ */
165
+ describe('table-level constraints', () => {
166
+ const table = {
167
+ table_name: 'user_roles',
168
+ add_base_columns: false,
169
+ columns: [
170
+ { name: 'user_id', type: 'uuid' },
171
+ { name: 'role_id', type: 'uuid' },
172
+ ],
173
+ constraints: {
174
+ unique: [{ name: 'unique_user_role', columns: ['user_id', 'role_id'] }],
175
+ },
176
+ }
177
+
178
+ it('emits a composite unique constraint that was declared', () => {
179
+ const code = generateTableCode(table as never, [], [])
180
+ expect(code).toContain("unique('unique_user_role')")
181
+ expect(code).toContain('t.userId')
182
+ expect(code).toContain('t.roleId')
183
+ })
184
+
185
+ it('emits a check constraint as the config wrote it', () => {
186
+ const code = generateTableCode(
187
+ {
188
+ ...table,
189
+ constraints: { check: [{ name: 'band_range', expression: 'band between 1 and 8' }] },
190
+ } as never,
191
+ [],
192
+ []
193
+ )
194
+ expect(code).toContain("check('band_range'")
195
+ expect(code).toContain('band between 1 and 8')
196
+ })
197
+
198
+ /** A constraint over a column that is not there would fail at push time. */
199
+ it('leaves out a constraint naming a column the table does not have', () => {
200
+ const code = generateTableCode(
201
+ {
202
+ ...table,
203
+ constraints: { unique: [{ name: 'ghost', columns: ['user_id', 'not_a_column'] }] },
204
+ } as never,
205
+ [],
206
+ []
207
+ )
208
+ expect(code).not.toContain("unique('ghost')")
209
+ })
210
+
211
+ it('adds no constraint block to a table that declares none', () => {
212
+ const code = generateTableCode({ ...table, constraints: undefined } as never, [], [])
213
+ expect(code).not.toContain('unique(')
214
+ expect(code).not.toContain('check(')
215
+ })
216
+ })
@@ -330,7 +330,15 @@ export function generateColumnCode(
330
330
  return code
331
331
  }
332
332
 
333
- function generateTableCode(table: TableDef, allTables: string[], allTableDefs: TableDef[]): string {
333
+ /**
334
+ * Exported for the tests that hold this generator to what the config declares —
335
+ * the table-level `constraints` in particular, which were parsed and dropped.
336
+ */
337
+ export function generateTableCode(
338
+ table: TableDef,
339
+ allTables: string[],
340
+ allTableDefs: TableDef[]
341
+ ): string {
334
342
  const tableName = toCamelCase(table.table_name)
335
343
  const TableName = tableName.charAt(0).toUpperCase() + tableName.slice(1)
336
344
  const columns = table.add_base_columns
@@ -369,8 +377,31 @@ function generateTableCode(table: TableDef, allTables: string[], allTableDefs: T
369
377
  code += '];\n\n'
370
378
  }
371
379
 
380
+ /*
381
+ * Table-level `constraints`, which were parsed and then dropped.
382
+ *
383
+ * `TableDef.constraints` has been in the config shape all along and nothing
384
+ * ever read it: twelve tables declare a composite unique key and not one of
385
+ * them exists in any database. That is not a missing nicety — it is the
386
+ * reason a user could hold the same role twice, a reader could react to the
387
+ * same article twice instead of changing their reaction, and an integration
388
+ * connection could be created twice under the identical slug (seen live: two
389
+ * connections named the same, and an import that matched by slug wrote to
390
+ * whichever one it found first).
391
+ *
392
+ * Emitted as a unique CONSTRAINT rather than a unique index, because that is
393
+ * what the config says and because `ON CONFLICT (cols)` needs one to target.
394
+ * A check constraint is emitted as written; the config owns its SQL.
395
+ */
396
+ const uniqueConstraints = (table.constraints?.unique ?? []).filter((c) =>
397
+ c.columns.every((col) => columns.some((defined) => defined.name === col))
398
+ )
399
+ const checkConstraints = table.constraints?.check ?? []
400
+ const hasTableExtras =
401
+ validIndexes.length > 0 || uniqueConstraints.length > 0 || checkConstraints.length > 0
402
+
372
403
  code += `export const ${tableName} = pgTable('${table.table_name}', ${tableName}Columns`
373
- if (validIndexes.length > 0) {
404
+ if (hasTableExtras) {
374
405
  code += `, (t) => [\n`
375
406
  for (const idx of validIndexes) {
376
407
  const idxName = idx.name || `${table.table_name}_${idx.columns.join('_')}_idx`
@@ -381,6 +412,13 @@ function generateTableCode(table: TableDef, allTables: string[], allTableDefs: T
381
412
  code += `\tindex('${idxName}').on(${cols}),\n`
382
413
  }
383
414
  }
415
+ for (const constraint of uniqueConstraints) {
416
+ const cols = constraint.columns.map((c) => `t.${toCamelCase(c)}`).join(', ')
417
+ code += `\tunique('${constraint.name}').on(${cols}),\n`
418
+ }
419
+ for (const constraint of checkConstraints) {
420
+ code += `\tcheck('${constraint.name}', sql\`${constraint.expression}\`),\n`
421
+ }
384
422
  code += `]`
385
423
  }
386
424
  code += ');\n\n'
@@ -480,6 +518,10 @@ function generateSchemaFile(tables: TableDef[]): string {
480
518
  imports.add('boolean')
481
519
  imports.add('index')
482
520
  imports.add('uniqueIndex')
521
+ // Table-level constraints from `constraints` — a composite unique key is a
522
+ // CONSTRAINT, not an index, and a check needs `sql` for its expression.
523
+ imports.add('unique')
524
+ imports.add('check')
483
525
 
484
526
  for (const table of tables) {
485
527
  const columns = table.add_base_columns