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.
- package/dist/api/sonamu.d.ts.map +1 -1
- package/dist/api/sonamu.js +2 -3
- package/dist/auth/auth-generator.d.ts +8 -0
- package/dist/auth/auth-generator.d.ts.map +1 -1
- package/dist/auth/auth-generator.js +33 -1
- package/dist/auth/better-auth-entities.d.ts.map +1 -1
- package/dist/auth/better-auth-entities.js +12 -2
- package/dist/bin/cli.js +18 -3
- package/dist/cone/cone-generator.js +10 -4
- package/dist/database/knex.d.ts.map +1 -1
- package/dist/database/knex.js +64 -2
- package/dist/database/puri.d.ts +9 -1
- package/dist/database/puri.d.ts.map +1 -1
- package/dist/database/puri.js +42 -1
- package/dist/database/puri.types.d.ts +2 -0
- package/dist/database/puri.types.d.ts.map +1 -1
- package/dist/database/puri.types.js +6 -2
- package/dist/entity/entity-manager.d.ts +149 -1
- package/dist/entity/entity-manager.d.ts.map +1 -1
- package/dist/entity/entity-manager.js +68 -4
- package/dist/migration/__tests__/code-generation.search-text.test.js +435 -0
- package/dist/migration/code-generation.d.ts.map +1 -1
- package/dist/migration/code-generation.js +696 -32
- package/dist/migration/migration-set.js +3 -1
- package/dist/migration/postgresql-schema-reader.d.ts +16 -2
- package/dist/migration/postgresql-schema-reader.d.ts.map +1 -1
- package/dist/migration/postgresql-schema-reader.js +281 -7
- package/dist/stream/sse.js +5 -3
- package/dist/template/__tests__/generated.template.search-text.test.js +99 -0
- package/dist/template/generated.template.test-d.js +24 -0
- package/dist/template/implementations/generated.template.d.ts.map +1 -1
- package/dist/template/implementations/generated.template.js +2 -2
- package/dist/template/implementations/init_types.template.d.ts.map +1 -1
- package/dist/template/implementations/init_types.template.js +11 -3
- package/dist/template/zod-converter.d.ts.map +1 -1
- package/dist/template/zod-converter.js +6 -2
- package/dist/testing/dev-test-routes.d.ts.map +1 -1
- package/dist/testing/dev-test-routes.js +5 -3
- package/dist/testing/fixture-generator.d.ts +13 -0
- package/dist/testing/fixture-generator.d.ts.map +1 -1
- package/dist/testing/fixture-generator.js +105 -8
- package/dist/testing/fixture-manager.d.ts.map +1 -1
- package/dist/testing/fixture-manager.js +19 -2
- package/dist/types/__tests__/entity-json-schema-search-text.test.js +256 -0
- package/dist/types/types.d.ts +494 -1
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/types.js +117 -13
- package/dist/ui/api.d.ts.map +1 -1
- package/dist/ui/api.js +14 -2
- package/dist/ui/cdd-service.d.ts +16 -14
- package/dist/ui/cdd-service.d.ts.map +1 -1
- package/dist/ui/cdd-service.js +145 -37
- package/dist/ui/cdd-types.d.ts +60 -0
- package/dist/ui/cdd-types.d.ts.map +1 -0
- package/dist/ui/cdd-types.js +3 -0
- package/dist/ui-web/assets/index-D4XFBV-f.css +1 -0
- package/dist/ui-web/assets/{index-CQ_S40bD.js → index-D_19-Pi4.js} +87 -87
- package/dist/ui-web/index.html +2 -2
- package/package.json +7 -3
- package/src/api/sonamu.ts +1 -2
- package/src/auth/auth-generator.ts +38 -0
- package/src/auth/better-auth-entities.ts +18 -1
- package/src/bin/cli.ts +15 -1
- package/src/cone/cone-generator.ts +9 -3
- package/src/database/knex.ts +62 -4
- package/src/database/puri.ts +71 -0
- package/src/database/puri.types.ts +2 -0
- package/src/entity/entity-manager.ts +95 -3
- package/src/migration/__tests__/code-generation.search-text.test.ts +390 -0
- package/src/migration/code-generation.ts +848 -34
- package/src/migration/migration-set.ts +2 -0
- package/src/migration/postgresql-schema-reader.ts +366 -9
- package/src/skills/sonamu/auth-migration.md +80 -0
- package/src/skills/sonamu/cdd.md +148 -28
- package/src/skills/sonamu/cone.md +16 -0
- package/src/skills/sonamu/entity-relations.md +1 -1
- package/src/skills/sonamu/fixture-cli.md +4 -0
- package/src/skills/sonamu/frontend.md +65 -0
- package/src/skills/sonamu/migration.md +3 -1
- package/src/skills/sonamu/model.md +28 -0
- package/src/skills/sonamu/workflow.md +12 -5
- package/src/stream/sse.ts +4 -2
- package/src/template/__tests__/generated.template.search-text.test.ts +89 -0
- package/src/template/generated.template.test-d.ts +46 -0
- package/src/template/implementations/generated.template.ts +4 -1
- package/src/template/implementations/init_types.template.ts +20 -5
- package/src/template/zod-converter.ts +5 -0
- package/src/testing/dev-test-routes.ts +4 -2
- package/src/testing/fixture-generator.ts +157 -9
- package/src/testing/fixture-manager.ts +15 -1
- package/src/types/__tests__/entity-json-schema-search-text.test.ts +179 -0
- package/src/types/types.ts +168 -12
- package/src/ui/api.ts +24 -1
- package/src/ui/cdd-service.ts +195 -55
- package/src/ui/cdd-types.ts +73 -0
- package/dist/ui-web/assets/index-egkMxKos.css +0 -1
|
@@ -52,6 +52,8 @@ type PgIndex = {
|
|
|
52
52
|
nulls_first: boolean;
|
|
53
53
|
sort_order: "ASC" | "DESC";
|
|
54
54
|
nulls_not_distinct: boolean;
|
|
55
|
+
column_order: number;
|
|
56
|
+
index_definition: string;
|
|
55
57
|
};
|
|
56
58
|
|
|
57
59
|
type PgForeign = {
|
|
@@ -63,14 +65,21 @@ type PgForeign = {
|
|
|
63
65
|
delete_rule: string;
|
|
64
66
|
};
|
|
65
67
|
|
|
68
|
+
type RawCapableKnex = Pick<Knex, "raw">;
|
|
69
|
+
|
|
66
70
|
class PostgreSQLSchemaReaderClass {
|
|
71
|
+
private readonly genericIndexTypes = new Set(["btree", "hash", "gin", "gist", "pgroonga"]);
|
|
72
|
+
|
|
67
73
|
/**
|
|
68
74
|
* DB에서 테이블 정보를 읽어서 MigrationSet을 만들어옵니다.
|
|
69
75
|
* @param compareDB Knex 인스턴스
|
|
70
76
|
* @param table 테이블 이름
|
|
71
77
|
* @returns MigrationSet 객체
|
|
72
78
|
*/
|
|
73
|
-
async getMigrationSetFromDB(
|
|
79
|
+
async getMigrationSetFromDB(
|
|
80
|
+
compareDB: RawCapableKnex,
|
|
81
|
+
table: string,
|
|
82
|
+
): Promise<MigrationSet | null> {
|
|
74
83
|
let dbColumns: PgColumn[], dbIndexes: PgIndex[], dbForeigns: PgForeign[];
|
|
75
84
|
try {
|
|
76
85
|
[dbColumns, dbIndexes, dbForeigns] = await this.readTable(compareDB, table);
|
|
@@ -154,18 +163,36 @@ class PostgreSQLSchemaReaderClass {
|
|
|
154
163
|
|
|
155
164
|
// indexes 처리
|
|
156
165
|
const indexes: MigrationIndex[] = Object.keys(dbIndexesGroup).map((indexName) => {
|
|
157
|
-
const currentIndexes = dbIndexesGroup[indexName]
|
|
166
|
+
const currentIndexes = dbIndexesGroup[indexName]?.toSorted(
|
|
167
|
+
(left, right) => left.column_order - right.column_order,
|
|
168
|
+
);
|
|
158
169
|
assert(currentIndexes);
|
|
159
170
|
|
|
160
171
|
const firstIndex = currentIndexes[0];
|
|
161
|
-
const
|
|
172
|
+
const parsedIndexDefinition = this.parseIndexDefinition(firstIndex.index_definition);
|
|
173
|
+
const restoredIndexType = this.restoreMigrationIndexType(
|
|
174
|
+
firstIndex,
|
|
175
|
+
parsedIndexDefinition.accessMethod,
|
|
176
|
+
);
|
|
177
|
+
const using = this.restoreGenericUsing(
|
|
178
|
+
parsedIndexDefinition.accessMethod ?? firstIndex.index_type,
|
|
179
|
+
);
|
|
162
180
|
|
|
163
181
|
return {
|
|
164
|
-
type,
|
|
182
|
+
type: restoredIndexType,
|
|
165
183
|
name: indexName,
|
|
166
184
|
columns: currentIndexes.map((idx) => ({
|
|
167
185
|
name: idx.column_name,
|
|
168
|
-
...(
|
|
186
|
+
...(this.extractIndexColumnOpclass(
|
|
187
|
+
parsedIndexDefinition.columnDefinitions[idx.column_order - 1],
|
|
188
|
+
)
|
|
189
|
+
? {
|
|
190
|
+
opclass: this.extractIndexColumnOpclass(
|
|
191
|
+
parsedIndexDefinition.columnDefinitions[idx.column_order - 1],
|
|
192
|
+
),
|
|
193
|
+
}
|
|
194
|
+
: {}),
|
|
195
|
+
...(using === "btree"
|
|
169
196
|
? {
|
|
170
197
|
sortOrder: idx.sort_order,
|
|
171
198
|
nullsFirst: idx.nulls_first,
|
|
@@ -174,7 +201,8 @@ class PostgreSQLSchemaReaderClass {
|
|
|
174
201
|
})),
|
|
175
202
|
|
|
176
203
|
nullsNotDistinct: firstIndex.nulls_not_distinct,
|
|
177
|
-
using
|
|
204
|
+
...(using ? { using } : {}),
|
|
205
|
+
...this.parseVectorIndexOptions(restoredIndexType, parsedIndexDefinition.withOptions),
|
|
178
206
|
};
|
|
179
207
|
});
|
|
180
208
|
|
|
@@ -214,7 +242,7 @@ class PostgreSQLSchemaReaderClass {
|
|
|
214
242
|
* 기존 테이블 읽어서 cols, indexes, foreigns 반환
|
|
215
243
|
*/
|
|
216
244
|
async readTable(
|
|
217
|
-
compareDB:
|
|
245
|
+
compareDB: RawCapableKnex,
|
|
218
246
|
tableName: string,
|
|
219
247
|
): Promise<[PgColumn[], PgIndex[], PgForeign[]]> {
|
|
220
248
|
// Columns 조회 (Generated Column 정보 포함)
|
|
@@ -278,7 +306,9 @@ class PostgreSQLSchemaReaderClass {
|
|
|
278
306
|
WHEN (u.opt & 1) = 1 THEN 'DESC'
|
|
279
307
|
ELSE 'ASC'
|
|
280
308
|
END AS sort_order,
|
|
281
|
-
ix.indnullsnotdistinct AS nulls_not_distinct
|
|
309
|
+
ix.indnullsnotdistinct AS nulls_not_distinct,
|
|
310
|
+
u.ord AS column_order,
|
|
311
|
+
pg_get_indexdef(ix.indexrelid) AS index_definition
|
|
282
312
|
FROM pg_class t
|
|
283
313
|
JOIN pg_index ix ON t.oid = ix.indrelid
|
|
284
314
|
JOIN pg_class i ON i.oid = ix.indexrelid
|
|
@@ -339,6 +369,333 @@ class PostgreSQLSchemaReaderClass {
|
|
|
339
369
|
return [columns, indexes, foreigns];
|
|
340
370
|
}
|
|
341
371
|
|
|
372
|
+
private restoreMigrationIndexType(
|
|
373
|
+
index: Pick<PgIndex, "is_unique" | "index_type">,
|
|
374
|
+
accessMethod?: string,
|
|
375
|
+
): MigrationIndex["type"] {
|
|
376
|
+
const resolvedAccessMethod = (accessMethod ?? index.index_type).toLowerCase();
|
|
377
|
+
|
|
378
|
+
if (resolvedAccessMethod === "hnsw" || resolvedAccessMethod === "ivfflat") {
|
|
379
|
+
return resolvedAccessMethod;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return index.is_unique ? "unique" : "index";
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private restoreGenericUsing(
|
|
386
|
+
accessMethod: string | undefined,
|
|
387
|
+
): MigrationIndex["using"] | undefined {
|
|
388
|
+
if (!accessMethod) {
|
|
389
|
+
return undefined;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const normalized = accessMethod.toLowerCase();
|
|
393
|
+
if (!this.genericIndexTypes.has(normalized)) {
|
|
394
|
+
return undefined;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return normalized as MigrationIndex["using"];
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
private parseVectorIndexOptions(
|
|
401
|
+
type: MigrationIndex["type"],
|
|
402
|
+
withOptions: Record<string, string>,
|
|
403
|
+
): Pick<MigrationIndex, "m" | "efConstruction" | "lists"> {
|
|
404
|
+
if (type === "hnsw") {
|
|
405
|
+
return {
|
|
406
|
+
...(this.parseIntegerOption(withOptions.m) !== undefined
|
|
407
|
+
? { m: this.parseIntegerOption(withOptions.m) }
|
|
408
|
+
: {}),
|
|
409
|
+
...(this.parseIntegerOption(withOptions.ef_construction) !== undefined
|
|
410
|
+
? { efConstruction: this.parseIntegerOption(withOptions.ef_construction) }
|
|
411
|
+
: {}),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (type === "ivfflat") {
|
|
416
|
+
return {
|
|
417
|
+
...(this.parseIntegerOption(withOptions.lists) !== undefined
|
|
418
|
+
? { lists: this.parseIntegerOption(withOptions.lists) }
|
|
419
|
+
: {}),
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return {};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private parseIntegerOption(value: string | undefined): number | undefined {
|
|
427
|
+
if (!value) {
|
|
428
|
+
return undefined;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const parsed = Number.parseInt(value, 10);
|
|
432
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private extractIndexColumnOpclass(columnDefinition: string | undefined): string | undefined {
|
|
436
|
+
if (!columnDefinition) {
|
|
437
|
+
return undefined;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const trimmed = columnDefinition
|
|
441
|
+
.replace(/\s+NULLS\s+(FIRST|LAST)\s*$/i, "")
|
|
442
|
+
.replace(/\s+(ASC|DESC)\s*$/i, "")
|
|
443
|
+
.trim();
|
|
444
|
+
const tokens = this.tokenizeTopLevel(trimmed);
|
|
445
|
+
|
|
446
|
+
if (tokens.length < 2) {
|
|
447
|
+
return undefined;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (tokens[tokens.length - 2]?.toUpperCase() === "COLLATE") {
|
|
451
|
+
return undefined;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return tokens.at(-1);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private parseIndexDefinition(indexDefinition: string): {
|
|
458
|
+
accessMethod?: string;
|
|
459
|
+
columnDefinitions: string[];
|
|
460
|
+
withOptions: Record<string, string>;
|
|
461
|
+
} {
|
|
462
|
+
const accessMethod = indexDefinition.match(/\bUSING\s+([a-z_][\w]*)/i)?.[1]?.toLowerCase();
|
|
463
|
+
const usingMatchIndex = indexDefinition.search(/\bUSING\b/i);
|
|
464
|
+
const columnsStart = usingMatchIndex >= 0 ? indexDefinition.indexOf("(", usingMatchIndex) : -1;
|
|
465
|
+
const columnsEnd =
|
|
466
|
+
columnsStart >= 0 ? this.findMatchingParenthesis(indexDefinition, columnsStart) : -1;
|
|
467
|
+
const columnDefinitions =
|
|
468
|
+
columnsStart >= 0 && columnsEnd > columnsStart
|
|
469
|
+
? this.splitTopLevel(indexDefinition.slice(columnsStart + 1, columnsEnd), ",")
|
|
470
|
+
: [];
|
|
471
|
+
|
|
472
|
+
const withMatch = /\bWITH\s*\(/i.exec(indexDefinition);
|
|
473
|
+
const withStart = withMatch ? indexDefinition.indexOf("(", withMatch.index) : -1;
|
|
474
|
+
const withEnd = withStart >= 0 ? this.findMatchingParenthesis(indexDefinition, withStart) : -1;
|
|
475
|
+
const withOptions =
|
|
476
|
+
withStart >= 0 && withEnd > withStart
|
|
477
|
+
? this.parseIndexOptionEntries(indexDefinition.slice(withStart + 1, withEnd))
|
|
478
|
+
: {};
|
|
479
|
+
|
|
480
|
+
return {
|
|
481
|
+
accessMethod,
|
|
482
|
+
columnDefinitions,
|
|
483
|
+
withOptions,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
private parseIndexOptionEntries(optionSource: string): Record<string, string> {
|
|
488
|
+
return this.splitTopLevel(optionSource, ",").reduce<Record<string, string>>((result, entry) => {
|
|
489
|
+
const matched = entry.trim().match(/^([a-z_][\w]*)\s*=\s*(.+)$/i);
|
|
490
|
+
if (!matched) {
|
|
491
|
+
return result;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const [, key, rawValue] = matched;
|
|
495
|
+
result[key.toLowerCase()] = rawValue.trim().replace(/^['"]|['"]$/g, "");
|
|
496
|
+
return result;
|
|
497
|
+
}, {});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
private splitTopLevel(source: string, delimiter: string): string[] {
|
|
501
|
+
const items: string[] = [];
|
|
502
|
+
let start = 0;
|
|
503
|
+
let parenDepth = 0;
|
|
504
|
+
let bracketDepth = 0;
|
|
505
|
+
let inSingleQuote = false;
|
|
506
|
+
let inDoubleQuote = false;
|
|
507
|
+
|
|
508
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
509
|
+
const char = source[index];
|
|
510
|
+
const nextChar = source[index + 1];
|
|
511
|
+
|
|
512
|
+
if (char === "'" && !inDoubleQuote) {
|
|
513
|
+
if (inSingleQuote && nextChar === "'") {
|
|
514
|
+
index += 1;
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
inSingleQuote = !inSingleQuote;
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (char === '"' && !inSingleQuote) {
|
|
522
|
+
if (inDoubleQuote && nextChar === '"') {
|
|
523
|
+
index += 1;
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
inDoubleQuote = !inDoubleQuote;
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (inSingleQuote || inDoubleQuote) {
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (char === "(") {
|
|
535
|
+
parenDepth += 1;
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
if (char === ")") {
|
|
539
|
+
parenDepth -= 1;
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
if (char === "[") {
|
|
543
|
+
bracketDepth += 1;
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
if (char === "]") {
|
|
547
|
+
bracketDepth -= 1;
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (char === delimiter && parenDepth === 0 && bracketDepth === 0) {
|
|
552
|
+
items.push(source.slice(start, index).trim());
|
|
553
|
+
start = index + 1;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const tail = source.slice(start).trim();
|
|
558
|
+
if (tail.length > 0) {
|
|
559
|
+
items.push(tail);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return items;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
private tokenizeTopLevel(source: string): string[] {
|
|
566
|
+
const tokens: string[] = [];
|
|
567
|
+
let start = -1;
|
|
568
|
+
let parenDepth = 0;
|
|
569
|
+
let bracketDepth = 0;
|
|
570
|
+
let inSingleQuote = false;
|
|
571
|
+
let inDoubleQuote = false;
|
|
572
|
+
|
|
573
|
+
const pushToken = (endIndex: number) => {
|
|
574
|
+
if (start < 0) {
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const token = source.slice(start, endIndex).trim();
|
|
579
|
+
if (token.length > 0) {
|
|
580
|
+
tokens.push(token);
|
|
581
|
+
}
|
|
582
|
+
start = -1;
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
586
|
+
const char = source[index];
|
|
587
|
+
const nextChar = source[index + 1];
|
|
588
|
+
|
|
589
|
+
if (char === "'" && !inDoubleQuote) {
|
|
590
|
+
if (start < 0) {
|
|
591
|
+
start = index;
|
|
592
|
+
}
|
|
593
|
+
if (inSingleQuote && nextChar === "'") {
|
|
594
|
+
index += 1;
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
inSingleQuote = !inSingleQuote;
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (char === '"' && !inSingleQuote) {
|
|
602
|
+
if (start < 0) {
|
|
603
|
+
start = index;
|
|
604
|
+
}
|
|
605
|
+
if (inDoubleQuote && nextChar === '"') {
|
|
606
|
+
index += 1;
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
inDoubleQuote = !inDoubleQuote;
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (!inSingleQuote && !inDoubleQuote) {
|
|
614
|
+
if (char === "(") {
|
|
615
|
+
if (start < 0) {
|
|
616
|
+
start = index;
|
|
617
|
+
}
|
|
618
|
+
parenDepth += 1;
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
if (char === ")") {
|
|
622
|
+
parenDepth -= 1;
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
if (char === "[") {
|
|
626
|
+
if (start < 0) {
|
|
627
|
+
start = index;
|
|
628
|
+
}
|
|
629
|
+
bracketDepth += 1;
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
if (char === "]") {
|
|
633
|
+
bracketDepth -= 1;
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (/\s/.test(char) && parenDepth === 0 && bracketDepth === 0) {
|
|
638
|
+
pushToken(index);
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (start < 0) {
|
|
644
|
+
start = index;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
pushToken(source.length);
|
|
649
|
+
return tokens;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
private findMatchingParenthesis(source: string, openIndex: number): number {
|
|
653
|
+
let depth = 0;
|
|
654
|
+
let inSingleQuote = false;
|
|
655
|
+
let inDoubleQuote = false;
|
|
656
|
+
|
|
657
|
+
for (let index = openIndex; index < source.length; index += 1) {
|
|
658
|
+
const char = source[index];
|
|
659
|
+
const nextChar = source[index + 1];
|
|
660
|
+
|
|
661
|
+
if (char === "'" && !inDoubleQuote) {
|
|
662
|
+
if (inSingleQuote && nextChar === "'") {
|
|
663
|
+
index += 1;
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
inSingleQuote = !inSingleQuote;
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (char === '"' && !inSingleQuote) {
|
|
671
|
+
if (inDoubleQuote && nextChar === '"') {
|
|
672
|
+
index += 1;
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
inDoubleQuote = !inDoubleQuote;
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (inSingleQuote || inDoubleQuote) {
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (char === "(") {
|
|
684
|
+
depth += 1;
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (char === ")") {
|
|
689
|
+
depth -= 1;
|
|
690
|
+
if (depth === 0) {
|
|
691
|
+
return index;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
return -1;
|
|
697
|
+
}
|
|
698
|
+
|
|
342
699
|
/**
|
|
343
700
|
* 특정 테이블의 PK를 참조하는 다른 테이블의 FK 목록을 조회합니다.
|
|
344
701
|
* PK 타입 변경 시 관련 FK 제약조건을 삭제/복구하기 위해 사용됩니다.
|
|
@@ -395,7 +752,7 @@ class PostgreSQLSchemaReaderClass {
|
|
|
395
752
|
* pg_attribute의 atttypmod에서 차원 수를 추출합니다.
|
|
396
753
|
*/
|
|
397
754
|
private async getVectorDimensions(
|
|
398
|
-
compareDB:
|
|
755
|
+
compareDB: RawCapableKnex,
|
|
399
756
|
tableName: string,
|
|
400
757
|
): Promise<Record<string, number>> {
|
|
401
758
|
const query = `
|
|
@@ -575,9 +575,89 @@ better-auth의 camelCase(`phoneNumber`)가 아닌 snake_case(`phone_number`)를
|
|
|
575
575
|
- [ ] Migration 적용: `pnpm migration:apply`
|
|
576
576
|
- [ ] 전체 테스트 실행: `pnpm test`
|
|
577
577
|
|
|
578
|
+
## Better-auth 엔티티 Fixture 생성
|
|
579
|
+
|
|
580
|
+
### 생성 순서 (필수)
|
|
581
|
+
|
|
582
|
+
better-auth 엔티티는 FK 의존성 때문에 반드시 다음 순서로 fixture를 생성해야 한다.
|
|
583
|
+
|
|
584
|
+
```
|
|
585
|
+
User → Account → Session → Verification (선택)
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
Account, Session은 user_id(string FK)를 통해 User를 참조하므로 User가 먼저 생성되어야 한다.
|
|
589
|
+
|
|
590
|
+
### 생성 명령
|
|
591
|
+
|
|
592
|
+
```bash
|
|
593
|
+
# 1. User 먼저 생성
|
|
594
|
+
pnpm sonamu fixture gen --include User --count 10 --use-llm
|
|
595
|
+
|
|
596
|
+
# 2. Account 생성 (User에 의존)
|
|
597
|
+
pnpm sonamu fixture gen --include Account --count 10 --use-llm
|
|
598
|
+
|
|
599
|
+
# 3. Session 생성 (User에 의존)
|
|
600
|
+
pnpm sonamu fixture gen --include Session --count 10 --use-llm
|
|
601
|
+
|
|
602
|
+
# 또는 User를 포함하여 함께 생성 (자동 순서 정렬)
|
|
603
|
+
pnpm sonamu fixture gen --include User,Account,Session --count 10 --use-llm
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
### User.id 시퀀스 설정 필수
|
|
607
|
+
|
|
608
|
+
better-auth User 엔티티는 id가 string 타입이지만, fixture gen이 자동으로 숫자 시퀀스를 사용한다. **PHASE 0에서 users_id_seq를 생성하지 않았다면 fixture gen이 실패한다.**
|
|
609
|
+
|
|
610
|
+
```sql
|
|
611
|
+
-- 반드시 먼저 설정되어 있어야 함
|
|
612
|
+
CREATE SEQUENCE users_id_seq;
|
|
613
|
+
ALTER TABLE users ALTER COLUMN id SET DEFAULT nextval('users_id_seq')::text;
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
이미 설정되어 있어야 하지만 누락된 경우 위 쿼리를 실행한 후 fixture gen을 진행한다.
|
|
617
|
+
|
|
618
|
+
### cone.fixtureStrategy 설정 권장
|
|
619
|
+
|
|
620
|
+
User entity.json의 id prop에 `"fixtureStrategy": "sequence"`가 설정되어 있는지 확인한다:
|
|
621
|
+
|
|
622
|
+
```json
|
|
623
|
+
{
|
|
624
|
+
"name": "id",
|
|
625
|
+
"type": "string",
|
|
626
|
+
"cone": {
|
|
627
|
+
"fixtureStrategy": "sequence",
|
|
628
|
+
"note": "better-auth가 관리하는 사용자 ID (string 타입)"
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
### Account 생성 시 주의사항
|
|
634
|
+
|
|
635
|
+
Account는 credential 계정과 OAuth 계정의 구조가 다르다:
|
|
636
|
+
|
|
637
|
+
```typescript
|
|
638
|
+
// credential 계정 (이메일/비밀번호)
|
|
639
|
+
{
|
|
640
|
+
provider_id: "credential",
|
|
641
|
+
account_id: "user@example.com",
|
|
642
|
+
user_id: existingUserId,
|
|
643
|
+
password: hashedPassword,
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// OAuth 계정 (Google 등)
|
|
647
|
+
{
|
|
648
|
+
provider_id: "google",
|
|
649
|
+
account_id: "google-oauth-id-12345",
|
|
650
|
+
user_id: existingUserId,
|
|
651
|
+
// password 없음
|
|
652
|
+
}
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
`--use-llm`과 cone.note를 적절히 설정하면 LLM이 맥락에 맞는 provider_id와 account_id를 생성해준다.
|
|
656
|
+
|
|
578
657
|
## Related Skills
|
|
579
658
|
|
|
580
659
|
- migration: Migration 작성 기본, PK 타입 변경
|
|
581
660
|
- entity-basic: Entity 타입 정의
|
|
582
661
|
- entity-relations: BelongsToOne, HasMany 관계
|
|
583
662
|
- testing: 테스트 작성 패턴
|
|
663
|
+
- fixture-cli: Fixture 생성 CLI 사용법
|