sonamu 0.8.13 → 0.8.14

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 (96) hide show
  1. package/dist/api/sonamu.d.ts.map +1 -1
  2. package/dist/api/sonamu.js +2 -3
  3. package/dist/auth/auth-generator.d.ts +8 -0
  4. package/dist/auth/auth-generator.d.ts.map +1 -1
  5. package/dist/auth/auth-generator.js +33 -1
  6. package/dist/auth/better-auth-entities.d.ts.map +1 -1
  7. package/dist/auth/better-auth-entities.js +12 -2
  8. package/dist/bin/cli.js +18 -3
  9. package/dist/cone/cone-generator.js +10 -4
  10. package/dist/database/knex.d.ts.map +1 -1
  11. package/dist/database/knex.js +64 -2
  12. package/dist/database/puri.d.ts +9 -1
  13. package/dist/database/puri.d.ts.map +1 -1
  14. package/dist/database/puri.js +42 -1
  15. package/dist/database/puri.types.d.ts +2 -0
  16. package/dist/database/puri.types.d.ts.map +1 -1
  17. package/dist/database/puri.types.js +6 -2
  18. package/dist/entity/entity-manager.d.ts +149 -1
  19. package/dist/entity/entity-manager.d.ts.map +1 -1
  20. package/dist/entity/entity-manager.js +68 -4
  21. package/dist/migration/__tests__/code-generation.search-text.test.js +435 -0
  22. package/dist/migration/code-generation.d.ts.map +1 -1
  23. package/dist/migration/code-generation.js +696 -32
  24. package/dist/migration/migration-set.js +3 -1
  25. package/dist/migration/postgresql-schema-reader.d.ts +16 -2
  26. package/dist/migration/postgresql-schema-reader.d.ts.map +1 -1
  27. package/dist/migration/postgresql-schema-reader.js +281 -7
  28. package/dist/stream/sse.js +5 -3
  29. package/dist/template/__tests__/generated.template.search-text.test.js +99 -0
  30. package/dist/template/generated.template.test-d.js +24 -0
  31. package/dist/template/implementations/generated.template.d.ts.map +1 -1
  32. package/dist/template/implementations/generated.template.js +2 -2
  33. package/dist/template/implementations/init_types.template.d.ts.map +1 -1
  34. package/dist/template/implementations/init_types.template.js +11 -3
  35. package/dist/template/zod-converter.d.ts.map +1 -1
  36. package/dist/template/zod-converter.js +6 -2
  37. package/dist/testing/dev-test-routes.d.ts.map +1 -1
  38. package/dist/testing/dev-test-routes.js +5 -3
  39. package/dist/testing/fixture-generator.d.ts +13 -0
  40. package/dist/testing/fixture-generator.d.ts.map +1 -1
  41. package/dist/testing/fixture-generator.js +105 -8
  42. package/dist/testing/fixture-manager.d.ts.map +1 -1
  43. package/dist/testing/fixture-manager.js +19 -2
  44. package/dist/types/__tests__/entity-json-schema-search-text.test.js +256 -0
  45. package/dist/types/types.d.ts +494 -1
  46. package/dist/types/types.d.ts.map +1 -1
  47. package/dist/types/types.js +117 -13
  48. package/dist/ui/api.d.ts.map +1 -1
  49. package/dist/ui/api.js +14 -2
  50. package/dist/ui/cdd-service.d.ts +16 -14
  51. package/dist/ui/cdd-service.d.ts.map +1 -1
  52. package/dist/ui/cdd-service.js +145 -37
  53. package/dist/ui/cdd-types.d.ts +60 -0
  54. package/dist/ui/cdd-types.d.ts.map +1 -0
  55. package/dist/ui/cdd-types.js +3 -0
  56. package/dist/ui-web/assets/index-D4XFBV-f.css +1 -0
  57. package/dist/ui-web/assets/{index-CQ_S40bD.js → index-D_19-Pi4.js} +87 -87
  58. package/dist/ui-web/index.html +2 -2
  59. package/package.json +7 -3
  60. package/src/api/sonamu.ts +1 -2
  61. package/src/auth/auth-generator.ts +38 -0
  62. package/src/auth/better-auth-entities.ts +18 -1
  63. package/src/bin/cli.ts +15 -1
  64. package/src/cone/cone-generator.ts +9 -3
  65. package/src/database/knex.ts +62 -4
  66. package/src/database/puri.ts +71 -0
  67. package/src/database/puri.types.ts +2 -0
  68. package/src/entity/entity-manager.ts +95 -3
  69. package/src/migration/__tests__/code-generation.search-text.test.ts +390 -0
  70. package/src/migration/code-generation.ts +848 -34
  71. package/src/migration/migration-set.ts +2 -0
  72. package/src/migration/postgresql-schema-reader.ts +366 -9
  73. package/src/skills/sonamu/auth-migration.md +80 -0
  74. package/src/skills/sonamu/cdd.md +148 -28
  75. package/src/skills/sonamu/cone.md +16 -0
  76. package/src/skills/sonamu/entity-relations.md +1 -1
  77. package/src/skills/sonamu/fixture-cli.md +4 -0
  78. package/src/skills/sonamu/frontend.md +65 -0
  79. package/src/skills/sonamu/migration.md +3 -1
  80. package/src/skills/sonamu/model.md +28 -0
  81. package/src/skills/sonamu/workflow.md +12 -5
  82. package/src/stream/sse.ts +4 -2
  83. package/src/template/__tests__/generated.template.search-text.test.ts +89 -0
  84. package/src/template/generated.template.test-d.ts +46 -0
  85. package/src/template/implementations/generated.template.ts +4 -1
  86. package/src/template/implementations/init_types.template.ts +20 -5
  87. package/src/template/zod-converter.ts +5 -0
  88. package/src/testing/dev-test-routes.ts +4 -2
  89. package/src/testing/fixture-generator.ts +157 -9
  90. package/src/testing/fixture-manager.ts +15 -1
  91. package/src/types/__tests__/entity-json-schema-search-text.test.ts +179 -0
  92. package/src/types/types.ts +168 -12
  93. package/src/ui/api.ts +24 -1
  94. package/src/ui/cdd-service.ts +195 -55
  95. package/src/ui/cdd-types.ts +73 -0
  96. package/dist/ui-web/assets/index-egkMxKos.css +0 -1
@@ -0,0 +1,390 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { Sonamu } from "../../api";
3
+ import { Entity } from "../../entity/entity";
4
+ import { EntityManager } from "../../entity/entity-manager";
5
+ import type { MigrationSet } from "../../types/types";
6
+ import { setupBiome } from "../../utils/formatter";
7
+ import {
8
+ generateAlterCode,
9
+ generateCreateCode,
10
+ setMigrationIndexDefaults,
11
+ } from "../code-generation";
12
+ import { getMigrationSetFromEntity } from "../migration-set";
13
+
14
+ const TEST_API_ROOT = "/Users/Nebuleto/Workspace/sonamu/modules/sonamu";
15
+
16
+ setupBiome(TEST_API_ROOT);
17
+ Sonamu.apiRootPath = TEST_API_ROOT;
18
+
19
+ let entitySeq = 0;
20
+
21
+ async function registerEntity(
22
+ definition: Omit<
23
+ Parameters<typeof EntityManager.register>[0],
24
+ "id" | "table" | "title" | "subsets" | "enums"
25
+ > & {
26
+ id: string;
27
+ table: string;
28
+ },
29
+ ) {
30
+ entitySeq += 1;
31
+
32
+ const entity = {
33
+ ...definition,
34
+ id: `${definition.id}${entitySeq}`,
35
+ title: `${definition.id}${entitySeq}`,
36
+ table: `${definition.table}_${entitySeq}`,
37
+ subsets: {
38
+ A: definition.props.map((prop) => prop.name),
39
+ },
40
+ enums: {},
41
+ };
42
+
43
+ await EntityManager.register(entity, {
44
+ deferSearchTextJsonSourceValidation: true,
45
+ });
46
+
47
+ return EntityManager.get(entity.id);
48
+ }
49
+
50
+ function buildDbSetWithGeneratedSearchText(
51
+ entitySet: MigrationSet,
52
+ expression: string,
53
+ ): MigrationSet {
54
+ return {
55
+ ...entitySet,
56
+ columns: entitySet.columns.map((column) =>
57
+ column.name === "search_text"
58
+ ? {
59
+ ...column,
60
+ generated: {
61
+ type: "STORED",
62
+ expression,
63
+ },
64
+ }
65
+ : column,
66
+ ),
67
+ };
68
+ }
69
+
70
+ describe("code-generation searchText/opclass DDL", () => {
71
+ test("searchText가 없으면 generated/helper SQL을 추가하지 않고 vector index를 유지해야 한다", async () => {
72
+ const entity = await registerEntity({
73
+ id: "CodeGenerationNoSearchText",
74
+ table: "code_generation_no_search_text",
75
+ props: [
76
+ { name: "id", type: "integer" },
77
+ { name: "title", type: "string" },
78
+ { name: "embedding", type: "vector", dimensions: 1536 },
79
+ ],
80
+ indexes: [
81
+ {
82
+ type: "hnsw",
83
+ name: "code_generation_no_search_text_embedding_hnsw",
84
+ columns: [{ name: "embedding", vectorOps: "vector_cosine_ops" }],
85
+ },
86
+ ],
87
+ });
88
+
89
+ const [migration] = await generateCreateCode(getMigrationSetFromEntity(entity));
90
+
91
+ expect(migration.formatted).not.toContain("GENERATED ALWAYS AS");
92
+ expect(migration.formatted).not.toContain("sonamu_text_array_agg");
93
+ expect(migration.formatted).not.toContain("sonamu_jsonb_array_agg");
94
+ expect(migration.formatted).toContain(
95
+ "USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64)",
96
+ );
97
+ });
98
+
99
+ test("searchText generated DDL, helper 함수, generic opclass를 올바르게 출력해야 한다", async () => {
100
+ const entity = await registerEntity({
101
+ id: "CodeGenerationSearchText",
102
+ table: "code_generation_search_text",
103
+ props: [
104
+ { name: "id", type: "integer" },
105
+ { name: "title_ci", type: "string" },
106
+ { name: "code_cs", type: "string" },
107
+ { name: "tags_ci", type: "string[]" },
108
+ { name: "tags_cs", type: "string[]" },
109
+ { name: "aliases_ci", type: "json", id: "StringArrayCi" },
110
+ { name: "aliases_cs", type: "json", id: "StringArrayCs" },
111
+ {
112
+ name: "search_text",
113
+ type: "searchText",
114
+ sourceColumns: [
115
+ { name: "title_ci", caseInsensitive: true },
116
+ { name: "code_cs", caseInsensitive: false },
117
+ { name: "tags_ci", caseInsensitive: true },
118
+ { name: "tags_cs", caseInsensitive: false },
119
+ { name: "aliases_ci", caseInsensitive: true },
120
+ { name: "aliases_cs", caseInsensitive: false },
121
+ ],
122
+ },
123
+ ],
124
+ indexes: [
125
+ {
126
+ type: "index",
127
+ name: "code_generation_search_text_search_text_index",
128
+ using: "gin",
129
+ columns: [{ name: "search_text", opclass: "gin_trgm_ops" }],
130
+ },
131
+ ],
132
+ });
133
+
134
+ const [migration] = await generateCreateCode(getMigrationSetFromEntity(entity));
135
+
136
+ expect(migration.formatted).toContain(
137
+ "CREATE OR REPLACE FUNCTION sonamu_text_array_agg(arr text[], ci boolean DEFAULT true)",
138
+ );
139
+ expect(migration.formatted).toContain(
140
+ "CREATE OR REPLACE FUNCTION sonamu_jsonb_array_agg(arr jsonb, ci boolean DEFAULT true)",
141
+ );
142
+ expect(migration.formatted).toContain(
143
+ "ADD COLUMN \"search_text\" text GENERATED ALWAYS AS (trim(lower(COALESCE(title_ci, '')) || ' ' || COALESCE(code_cs, '')",
144
+ );
145
+ expect(migration.formatted).toContain("COALESCE(sonamu_text_array_agg(tags_ci), '')");
146
+ expect(migration.formatted).toContain("COALESCE(sonamu_text_array_agg(tags_cs, false), '')");
147
+ expect(migration.formatted).toContain("COALESCE(sonamu_jsonb_array_agg(aliases_ci), '')");
148
+ expect(migration.formatted).toContain(
149
+ "COALESCE(sonamu_jsonb_array_agg(aliases_cs, false), '')",
150
+ );
151
+ expect(migration.formatted).toContain(
152
+ "CREATE INDEX code_generation_search_text_search_text_index ON",
153
+ );
154
+ expect(migration.formatted).toContain("USING gin(search_text gin_trgm_ops);");
155
+ });
156
+
157
+ test("searchText generated expression 변경 시 alter path에서 helper와 index를 함께 재생성해야 한다", async () => {
158
+ const entity = await registerEntity({
159
+ id: "CodeGenerationSearchTextAlter",
160
+ table: "code_generation_search_text_alter",
161
+ props: [
162
+ { name: "id", type: "integer" },
163
+ { name: "title_ci", type: "string" },
164
+ { name: "tags_ci", type: "string[]" },
165
+ {
166
+ name: "search_text",
167
+ type: "searchText",
168
+ sourceColumns: [
169
+ { name: "title_ci", caseInsensitive: false },
170
+ { name: "tags_ci", caseInsensitive: true },
171
+ ],
172
+ },
173
+ ],
174
+ indexes: [
175
+ {
176
+ type: "index",
177
+ name: "code_generation_search_text_alter_search_text_index",
178
+ using: "gin",
179
+ columns: [{ name: "search_text", opclass: "gin_trgm_ops" }],
180
+ },
181
+ ],
182
+ });
183
+
184
+ const entitySet = getMigrationSetFromEntity(entity);
185
+ const dbSet = buildDbSetWithGeneratedSearchText(
186
+ entitySet,
187
+ `trim(lower(COALESCE(title_ci, '')))`,
188
+ );
189
+
190
+ const [migration] = await generateAlterCode(entitySet, dbSet);
191
+
192
+ expect(migration.formatted).toContain(
193
+ 'table.dropIndex(["search_text"], "code_generation_search_text_alter_search_text_index")',
194
+ );
195
+ expect(migration.formatted).toContain('table.dropColumns("search_text")');
196
+ expect(migration.formatted).toContain(
197
+ "CREATE OR REPLACE FUNCTION sonamu_text_array_agg(arr text[], ci boolean DEFAULT true)",
198
+ );
199
+ expect(migration.formatted).toContain(
200
+ `ADD COLUMN "search_text" text GENERATED ALWAYS AS (trim(COALESCE(title_ci, '') || ' ' || COALESCE(sonamu_text_array_agg(tags_ci), ''))) STORED NOT NULL`,
201
+ );
202
+ expect(migration.formatted).toContain(
203
+ "CREATE INDEX code_generation_search_text_alter_search_text_index ON",
204
+ );
205
+ expect(migration.formatted).toContain(
206
+ `ADD COLUMN "search_text" text GENERATED ALWAYS AS (trim(lower(COALESCE(title_ci, '')))) STORED NOT NULL`,
207
+ );
208
+ });
209
+
210
+ test("searchText generated expression이 Postgres canonical form과 semantically equivalent면 no-op이어야 한다", async () => {
211
+ const entity = await registerEntity({
212
+ id: "CodeGenerationSearchTextCanonicalNoop",
213
+ table: "code_generation_search_text_canonical_noop",
214
+ props: [
215
+ { name: "id", type: "integer" },
216
+ { name: "username", type: "string" },
217
+ { name: "tags", type: "string[]" },
218
+ {
219
+ name: "search_text",
220
+ type: "searchText",
221
+ sourceColumns: [
222
+ { name: "username", caseInsensitive: false },
223
+ { name: "tags", caseInsensitive: true },
224
+ ],
225
+ },
226
+ ],
227
+ indexes: [
228
+ {
229
+ type: "index",
230
+ name: "code_generation_search_text_canonical_noop_search_text_index",
231
+ using: "gin",
232
+ columns: [{ name: "search_text", opclass: "gin_trgm_ops" }],
233
+ },
234
+ ],
235
+ });
236
+
237
+ const entitySet = getMigrationSetFromEntity(entity);
238
+ const dbSet = buildDbSetWithGeneratedSearchText(
239
+ {
240
+ ...entitySet,
241
+ indexes: entitySet.indexes.map(setMigrationIndexDefaults),
242
+ },
243
+ `TRIM(BOTH FROM ((COALESCE(username, ''::text)) || ' '::text) || COALESCE(sonamu_text_array_agg(tags, true), ''::text))`,
244
+ );
245
+
246
+ const migrations = await generateAlterCode(entitySet, dbSet);
247
+
248
+ expect(migrations).toHaveLength(0);
249
+ });
250
+
251
+ test("searchText caseInsensitive 변경만 있어도 alter path에서 재생성해야 한다", async () => {
252
+ const entity = await registerEntity({
253
+ id: "CodeGenerationSearchTextCaseInsensitiveAlter",
254
+ table: "code_generation_search_text_case_insensitive_alter",
255
+ props: [
256
+ { name: "id", type: "integer" },
257
+ { name: "title_ci", type: "string" },
258
+ {
259
+ name: "search_text",
260
+ type: "searchText",
261
+ sourceColumns: [{ name: "title_ci", caseInsensitive: false }],
262
+ },
263
+ ],
264
+ indexes: [
265
+ {
266
+ type: "index",
267
+ name: "code_generation_search_text_case_insensitive_alter_search_text_index",
268
+ using: "gin",
269
+ columns: [{ name: "search_text", opclass: "gin_trgm_ops" }],
270
+ },
271
+ ],
272
+ });
273
+
274
+ const entitySet = getMigrationSetFromEntity(entity);
275
+ const dbSet = buildDbSetWithGeneratedSearchText(
276
+ entitySet,
277
+ `TRIM(BOTH FROM lower(COALESCE(title_ci, ''::text)))`,
278
+ );
279
+
280
+ const [migration] = await generateAlterCode(entitySet, dbSet);
281
+
282
+ expect(migration.formatted).toContain("table.dropIndex(");
283
+ expect(migration.formatted).toContain(
284
+ '"code_generation_search_text_case_insensitive_alter_search_text_index"',
285
+ );
286
+ expect(migration.formatted).toContain('table.dropColumns("search_text")');
287
+ expect(migration.formatted).toContain(
288
+ `ADD COLUMN "search_text" text GENERATED ALWAYS AS (trim(COALESCE(title_ci, ''))) STORED NOT NULL`,
289
+ );
290
+ expect(migration.formatted).toContain("lower(COALESCE(title_ci");
291
+ });
292
+
293
+ test("searchText helper kind가 바뀌어도 down path는 복원 expression 기준 helper를 재생성해야 한다", async () => {
294
+ const entity = await registerEntity({
295
+ id: "CodeGenerationSearchTextRollbackHelperMismatch",
296
+ table: "code_generation_search_text_rollback_helper_mismatch",
297
+ props: [
298
+ { name: "id", type: "integer" },
299
+ { name: "tags", type: "string[]" },
300
+ { name: "aliases", type: "json", id: "RollbackHelperMismatchJson" },
301
+ {
302
+ name: "search_text",
303
+ type: "searchText",
304
+ sourceColumns: [{ name: "tags", caseInsensitive: true }],
305
+ },
306
+ ],
307
+ indexes: [
308
+ {
309
+ type: "index",
310
+ name: "code_generation_search_text_rollback_helper_mismatch_search_text_index",
311
+ using: "gin",
312
+ columns: [{ name: "search_text", opclass: "gin_trgm_ops" }],
313
+ },
314
+ ],
315
+ });
316
+
317
+ const entitySet = getMigrationSetFromEntity(entity);
318
+ const dbSet = buildDbSetWithGeneratedSearchText(
319
+ entitySet,
320
+ `trim(COALESCE(sonamu_jsonb_array_agg(aliases), ''))`,
321
+ );
322
+
323
+ const [migration] = await generateAlterCode(entitySet, dbSet);
324
+
325
+ expect(migration.formatted).toContain(
326
+ "CREATE OR REPLACE FUNCTION sonamu_text_array_agg(arr text[], ci boolean DEFAULT true)",
327
+ );
328
+ expect(migration.formatted).toContain(
329
+ `ADD COLUMN "search_text" text GENERATED ALWAYS AS (trim(COALESCE(sonamu_text_array_agg(tags), ''))) STORED NOT NULL`,
330
+ );
331
+ expect(migration.formatted).toContain(
332
+ "CREATE OR REPLACE FUNCTION sonamu_jsonb_array_agg(arr jsonb, ci boolean DEFAULT true)",
333
+ );
334
+ expect(migration.formatted).toContain(
335
+ `ADD COLUMN "search_text" text GENERATED ALWAYS AS (trim(COALESCE(sonamu_jsonb_array_agg(aliases), ''))) STORED NOT NULL`,
336
+ );
337
+ });
338
+
339
+ test("searchText가 제거되어도 down path는 이전 expression helper를 복원해야 한다", async () => {
340
+ const entity = await registerEntity({
341
+ id: "CodeGenerationSearchTextRollbackDropped",
342
+ table: "code_generation_search_text_rollback_dropped",
343
+ props: [
344
+ { name: "id", type: "integer" },
345
+ { name: "tags", type: "string[]" },
346
+ ],
347
+ indexes: [],
348
+ });
349
+
350
+ const previousEntity = new Entity({
351
+ ...entity.toJson(),
352
+ props: [
353
+ ...entity.toJson().props,
354
+ {
355
+ name: "search_text",
356
+ type: "searchText",
357
+ sourceColumns: [{ name: "tags", caseInsensitive: true }],
358
+ },
359
+ ],
360
+ indexes: [
361
+ {
362
+ type: "index",
363
+ name: "code_generation_search_text_rollback_dropped_search_text_index",
364
+ using: "gin",
365
+ columns: [{ name: "search_text", opclass: "gin_trgm_ops" }],
366
+ },
367
+ ],
368
+ });
369
+
370
+ const entitySet = getMigrationSetFromEntity(entity);
371
+ const dbSet = buildDbSetWithGeneratedSearchText(
372
+ getMigrationSetFromEntity(previousEntity),
373
+ `trim(COALESCE(sonamu_text_array_agg(tags), ''))`,
374
+ );
375
+
376
+ const [migration] = await generateAlterCode(entitySet, dbSet);
377
+
378
+ expect(migration.formatted).toContain('table.dropColumns("search_text")');
379
+ expect(migration.formatted).toContain(
380
+ "CREATE OR REPLACE FUNCTION sonamu_text_array_agg(arr text[], ci boolean DEFAULT true)",
381
+ );
382
+ expect(migration.formatted).toContain(
383
+ `ADD COLUMN "search_text" text GENERATED ALWAYS AS (trim(COALESCE(sonamu_text_array_agg(tags), ''))) STORED NOT NULL`,
384
+ );
385
+ expect(migration.formatted).toContain(
386
+ "CREATE INDEX code_generation_search_text_rollback_dropped_search_text_index ON",
387
+ );
388
+ expect(migration.formatted).toContain("USING gin(search_text gin_trgm_ops);");
389
+ });
390
+ });