nucleus-core-ts 0.10.14 → 0.10.15

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/dist/.build-ok CHANGED
@@ -1 +1 @@
1
- 0.10.14
1
+ 0.10.15
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nucleus-core-ts",
3
- "version": "0.10.14",
3
+ "version": "0.10.15",
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,10 @@
1
1
  import { describe, expect, it } from 'bun:test'
2
- import { generateColumnCode, generateTableCode, selectEmittedTables } from './generate-schema'
2
+ import {
3
+ generateColumnCode,
4
+ generateRelationsFile,
5
+ generateTableCode,
6
+ selectEmittedTables,
7
+ } from './generate-schema'
3
8
 
4
9
  const table = (table_name: string) => ({ table_name })
5
10
 
@@ -214,3 +219,57 @@ describe('table-level constraints', () => {
214
219
  expect(code).not.toContain('check(')
215
220
  })
216
221
  })
222
+
223
+ describe('generateRelationsFile', () => {
224
+ const branchTable = {
225
+ table_name: 'survey_branches',
226
+ columns: [
227
+ {
228
+ name: 'trigger_question_id',
229
+ type: 'uuid',
230
+ references: { table: 'survey_questions', column: 'id' },
231
+ },
232
+ {
233
+ name: 'target_question_id',
234
+ type: 'uuid',
235
+ references: { table: 'survey_questions', column: 'id' },
236
+ },
237
+ {
238
+ name: 'survey_id',
239
+ type: 'uuid',
240
+ references: { table: 'surveys', column: 'id' },
241
+ },
242
+ ],
243
+ }
244
+
245
+ it('names two references to the SAME table after their columns', () => {
246
+ /*
247
+ * Both used to be emitted as `surveyQuestions:`, so the object literal had
248
+ * the same key twice and the generated file did not compile — a config
249
+ * change that produces broken output rather than a wrong result. A branch
250
+ * whose trigger and target are both questions is what found it; `created_by`
251
+ * with `updated_by` would have found it just as well.
252
+ */
253
+ const code = generateRelationsFile([
254
+ branchTable,
255
+ { table_name: 'survey_questions' },
256
+ { table_name: 'surveys' },
257
+ ] as never)
258
+
259
+ expect(code).toContain('triggerQuestion: one(surveyQuestions')
260
+ expect(code).toContain('targetQuestion: one(surveyQuestions')
261
+ expect(code.match(/surveyQuestions: one\(/g)).toBeNull()
262
+ })
263
+
264
+ it('and leaves a lone reference named after its table, as before', () => {
265
+ // The control: renaming everything would churn every existing generated
266
+ // file and rename relations consumers already query by.
267
+ const code = generateRelationsFile([
268
+ branchTable,
269
+ { table_name: 'survey_questions' },
270
+ { table_name: 'surveys' },
271
+ ] as never)
272
+
273
+ expect(code).toContain('surveys: one(surveys')
274
+ })
275
+ })
@@ -607,7 +607,7 @@ function topologicalSort(tables: TableDef[], allTableNames: string[]): TableDef[
607
607
  return result
608
608
  }
609
609
 
610
- function generateRelationsFile(tables: TableDef[]): string {
610
+ export function generateRelationsFile(tables: TableDef[]): string {
611
611
  const allTableNames = new Set(tables.map((t) => t.table_name))
612
612
  const relationsByTable: Map<
613
613
  string,
@@ -640,9 +640,37 @@ function generateRelationsFile(tables: TableDef[]): string {
640
640
  const fromTable = toCamelCase(tableName)
641
641
  code += `export const ${fromTable}Relations = relations(${fromTable}, ({ one, many }) => ({\n`
642
642
 
643
+ /*
644
+ * How many of this table's foreign keys point at the same table.
645
+ *
646
+ * The relation used to be named after the TARGET TABLE, which is fine until
647
+ * a table references the same one twice — and then the generator emits two
648
+ * properties with the same key and the output does not compile:
649
+ *
650
+ * surveyQuestions: one(surveyQuestions, { fields: [...triggerQuestionId] }),
651
+ * surveyQuestions: one(surveyQuestions, { fields: [...targetQuestionId] }),
652
+ *
653
+ * That is not an exotic shape. `created_by`/`updated_by`, `from_user`/
654
+ * `to_user`, and here a branch's trigger question and its target question
655
+ * all land on it. Found when survey branching grew a second kind of target.
656
+ */
657
+ const referencesToTable = new Map<string, number>()
658
+ for (const rel of rels) {
659
+ referencesToTable.set(rel.to, (referencesToTable.get(rel.to) ?? 0) + 1)
660
+ }
661
+
643
662
  for (const rel of rels) {
644
663
  const toTable = toCamelCase(rel.to)
645
- const relationName = rel.isSelf ? 'parent' : toTable
664
+ // Only the ambiguous ones are renamed, so a config with one reference per
665
+ // table generates exactly what it generated before.
666
+ const ambiguous = (referencesToTable.get(rel.to) ?? 0) > 1
667
+ const relationName = ambiguous
668
+ ? // `trigger_question_id` -> `triggerQuestion`: the column says which of
669
+ // the two this is, which is the only thing that tells them apart.
670
+ toCamelCase(rel.fromCol.replace(/_id$/, ''))
671
+ : rel.isSelf
672
+ ? 'parent'
673
+ : toTable
646
674
  const fields = `${fromTable}.${toCamelCase(rel.fromCol)}`
647
675
  const references = `${toTable}.${toCamelCase(rel.toCol)}`
648
676