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.
|
|
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
|
-
|
|
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 (
|
|
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
|