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
@@ -260,6 +260,8 @@ function resolveEntityPropTypeToMigrationColumnType(prop: EntityProp): Migration
260
260
  return "uuid[]";
261
261
  case "json":
262
262
  return "json";
263
+ case "searchText":
264
+ return "string";
263
265
  case "vector":
264
266
  return "vector";
265
267
  case "vector[]":
@@ -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(compareDB: Knex, table: string): Promise<MigrationSet | null> {
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 type = firstIndex.is_unique ? "unique" : "index";
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
- ...(firstIndex.index_type === "btree"
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: firstIndex.index_type as "btree" | "hash" | "gin" | "gist" | "pgroonga" | undefined,
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: Knex,
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: Knex,
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 사용법