sonamu 0.5.2 → 0.5.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonamu",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "description": "Sonamu — TypeScript Fullstack API Framework",
5
5
  "keywords": [
6
6
  "typescript",
@@ -24,6 +24,10 @@
24
24
  "url": "https://github.com/ping-alive/sonamu.git"
25
25
  },
26
26
  "bin": "./dist/bin/cli-wrapper.js",
27
+ "files": [
28
+ "dist",
29
+ "src"
30
+ ],
27
31
  "dependencies": {
28
32
  "@aws-sdk/client-s3": "^3.921.0",
29
33
  "@aws-sdk/s3-request-presigner": "^3.921.0",
@@ -400,7 +400,7 @@ export function zodTypeToZodCode(zt: z.ZodType<any>): string {
400
400
  .join(",")}])`;
401
401
  case "enum":
402
402
  // NOTE: z.enum(["A", "B"])도 z.enum({ A: "A", B: "B" })로 처리됨.
403
- return `z.enum([${Object.entries((zt as z.ZodEnum).def.entries)
403
+ return `z.enum({${Object.entries((zt as z.ZodEnum).def.entries)
404
404
  .map(([key, val]) =>
405
405
  typeof val === "string" ? `${key}: "${val}"` : `${key}: ${val}`)
406
406
  .join(", ")}})`;
@@ -40,12 +40,16 @@ export type StreamDecoratorOptions = {
40
40
  guards?: GuardKey[];
41
41
  description?: string;
42
42
  };
43
+ export type UploadDecoratorOptions = {
44
+ mode?: "single" | "multiple";
45
+ };
43
46
  export const registeredApis: {
44
47
  modelName: string;
45
48
  methodName: string;
46
49
  path: string;
47
50
  options: ApiDecoratorOptions;
48
51
  streamOptions?: StreamDecoratorOptions;
52
+ uploadOptions?: UploadDecoratorOptions;
49
53
  }[] = [];
50
54
  export type ExtendedApi = {
51
55
  modelName: string;
@@ -53,6 +57,7 @@ export type ExtendedApi = {
53
57
  path: string;
54
58
  options: ApiDecoratorOptions;
55
59
  streamOptions?: StreamDecoratorOptions;
60
+ uploadOptions?: UploadDecoratorOptions;
56
61
  typeParameters: ApiParamType.TypeParam[];
57
62
  parameters: ApiParam[];
58
63
  returnType: ApiParamType;
@@ -176,13 +181,23 @@ export function transactional(options: TransactionalOptions = {}) {
176
181
  };
177
182
  }
178
183
 
179
- export function upload() {
184
+ export function upload(options: UploadDecoratorOptions = {}) {
180
185
  return function (
181
186
  _target: Object,
182
187
  _propertyKey: string,
183
188
  descriptor: PropertyDescriptor
184
189
  ) {
185
190
  const originalMethod = descriptor.value;
191
+ const modelName = _target.constructor.name.match(/(.+)Class$/)![1];
192
+ const methodName = _propertyKey;
193
+
194
+ // registeredApis에서 해당 API 찾아서 uploadOptions 추가
195
+ const existingApi = registeredApis.find(
196
+ (api) => api.modelName === modelName && api.methodName === methodName
197
+ );
198
+ if (existingApi) {
199
+ existingApi.uploadOptions = options;
200
+ }
186
201
 
187
202
  descriptor.value = async function (this: any, ...args: any[]) {
188
203
  const { request } = Sonamu.getContext();
@@ -196,20 +211,19 @@ export function upload() {
196
211
  throw new Error("Storage가 설정되지 않았습니다.");
197
212
  }
198
213
 
199
- if (request.file) {
200
- const rawFile = await request.file();
201
- if (rawFile) {
202
- const { FileStorage } = await import("../file-storage/file-storage");
203
- uploadContext.file = new FileStorage(rawFile, storage);
204
- }
205
- } else if (request.files) {
206
- const { FileStorage } = await import("../file-storage/file-storage");
214
+ const { FileStorage } = await import("../file-storage/file-storage");
215
+ if (options.mode === "multiple") {
207
216
  const rawFilesIterator = request.files();
208
217
  for await (const rawFile of rawFilesIterator) {
209
218
  if (rawFile) {
210
219
  uploadContext.files.push(new FileStorage(rawFile, storage));
211
220
  }
212
221
  }
222
+ } else {
223
+ const rawFile = await request.file();
224
+ if (rawFile) {
225
+ uploadContext.file = new FileStorage(rawFile, storage);
226
+ }
213
227
  }
214
228
 
215
229
  return Sonamu.uploadStorage.run({ uploadContext }, () => {
@@ -47,7 +47,7 @@ if (!existsSync(scriptPath)) {
47
47
 
48
48
  const result = spawnSync(
49
49
  process.execPath,
50
- ["--no-warnings", scriptPath, ...args],
50
+ ["-r", "source-map-support/register", "--no-warnings", scriptPath, ...args],
51
51
  {
52
52
  stdio: "inherit",
53
53
  }
@@ -17,9 +17,12 @@ export type TransactionalOptions = {
17
17
  };
18
18
 
19
19
  export class PuriWrapper<
20
- DBSchema extends DatabaseSchemaExtend = DatabaseSchemaExtend
20
+ DBSchema extends DatabaseSchemaExtend = DatabaseSchemaExtend,
21
21
  > {
22
- constructor(public knex: Knex, public upsertBuilder: UpsertBuilder) {}
22
+ constructor(
23
+ public knex: Knex,
24
+ public upsertBuilder: UpsertBuilder
25
+ ) {}
23
26
 
24
27
  raw(sql: string): Knex.Raw {
25
28
  return this.knex.raw(sql);
@@ -40,14 +43,14 @@ export class PuriWrapper<
40
43
  }
41
44
 
42
45
  async transaction<T>(
43
- callback: (trx: PuriWrapper) => Promise<T>,
46
+ callback: (trx: PuriTransactionWrapper) => Promise<T>,
44
47
  options: TransactionalOptions = {}
45
48
  ): Promise<T> {
46
49
  const { isolation, readOnly } = options;
47
50
 
48
51
  return this.knex.transaction(
49
52
  async (trx) => {
50
- return callback(new PuriWrapper(trx, this.upsertBuilder));
53
+ return callback(new PuriTransactionWrapper(trx, this.upsertBuilder));
51
54
  },
52
55
  { isolationLevel: isolation, readOnly }
53
56
  );
@@ -127,3 +130,20 @@ export class PuriWrapper<
127
130
  }
128
131
  }
129
132
  }
133
+
134
+ export class PuriTransactionWrapper extends PuriWrapper {
135
+ constructor(
136
+ public trx: Knex.Transaction,
137
+ public upsertBuilder: UpsertBuilder
138
+ ) {
139
+ super(trx, upsertBuilder);
140
+ }
141
+
142
+ async rollback(): Promise<void> {
143
+ await this.trx.rollback();
144
+ }
145
+
146
+ async commit(): Promise<void> {
147
+ await this.trx.commit();
148
+ }
149
+ }
@@ -224,7 +224,9 @@ export class Puri<
224
224
  }
225
225
 
226
226
  // WhereIn (조인된 테이블 컬럼도 지원)
227
- whereIn<TColumn extends AvailableColumns<TSchema, TTable, TOriginal, TJoined>>(
227
+ whereIn<
228
+ TColumn extends AvailableColumns<TSchema, TTable, TOriginal, TJoined>,
229
+ >(
228
230
  column: TColumn,
229
231
  values: ExtractColumnType<
230
232
  TSchema,
@@ -264,7 +266,10 @@ export class Puri<
264
266
 
265
267
  whereMatch<
266
268
  TColumn extends FulltextColumns<TSchema, TTable, TOriginal, TJoined>,
267
- >(column: TColumn, value: string): Puri<TSchema, TTable, TOriginal, TResult, TJoined> {
269
+ >(
270
+ column: TColumn,
271
+ value: string
272
+ ): Puri<TSchema, TTable, TOriginal, TResult, TJoined> {
268
273
  this.knexQuery.whereRaw(`MATCH (${String(column)}) AGAINST (?)`, [value]);
269
274
  return this;
270
275
  }
@@ -276,7 +281,9 @@ export class Puri<
276
281
  ) => WhereGroup<TSchema, TTable, TOriginal, TJoined>
277
282
  ): Puri<TSchema, TTable, TOriginal, TResult, TJoined> {
278
283
  this.knexQuery.where((builder) => {
279
- const group = new WhereGroup<TSchema, TTable, TOriginal, TJoined>(builder);
284
+ const group = new WhereGroup<TSchema, TTable, TOriginal, TJoined>(
285
+ builder
286
+ );
280
287
  callback(group);
281
288
  });
282
289
  return this;
@@ -288,7 +295,9 @@ export class Puri<
288
295
  ) => WhereGroup<TSchema, TTable, TOriginal, TJoined>
289
296
  ): Puri<TSchema, TTable, TOriginal, TResult, TJoined> {
290
297
  this.knexQuery.orWhere((builder) => {
291
- const group = new WhereGroup<TSchema, TTable, TOriginal, TJoined>(builder);
298
+ const group = new WhereGroup<TSchema, TTable, TOriginal, TJoined>(
299
+ builder
300
+ );
292
301
  callback(group);
293
302
  });
294
303
  return this;
@@ -297,12 +306,22 @@ export class Puri<
297
306
  // Join
298
307
  join<
299
308
  TJoinTable extends keyof TSchema,
300
- TLColumn extends AvailableColumns<TSchema, TTable, TOriginal, TJoined & Record<TJoinTable, TSchema[TJoinTable]>>,
301
- TRColumn extends AvailableColumns<TSchema, TTable, TOriginal, TJoined & Record<TJoinTable, TSchema[TJoinTable]>>,
309
+ TLColumn extends AvailableColumns<
310
+ TSchema,
311
+ TTable,
312
+ TOriginal,
313
+ TJoined & Record<TJoinTable, TSchema[TJoinTable]>
314
+ >,
315
+ TRColumn extends AvailableColumns<
316
+ TSchema,
317
+ TTable,
318
+ TOriginal,
319
+ TJoined & Record<TJoinTable, TSchema[TJoinTable]>
320
+ >,
302
321
  >(
303
322
  table: TJoinTable,
304
323
  left: TLColumn,
305
- right: TRColumn,
324
+ right: TRColumn
306
325
  ): Puri<
307
326
  TSchema,
308
327
  TTable,
@@ -327,7 +346,13 @@ export class Puri<
327
346
  alias: TAlias,
328
347
  left: string,
329
348
  right: string
330
- ): Puri<TSchema, TTable, TOriginal, TResult, TJoined & Record<TAlias, TSubResult>>;
349
+ ): Puri<
350
+ TSchema,
351
+ TTable,
352
+ TOriginal,
353
+ TResult,
354
+ TJoined & Record<TAlias, TSubResult>
355
+ >;
331
356
  join(
332
357
  table: string,
333
358
  left: string,
@@ -361,12 +386,22 @@ export class Puri<
361
386
 
362
387
  leftJoin<
363
388
  TJoinTable extends keyof TSchema,
364
- TLColumn extends AvailableColumns<TSchema, TTable, TOriginal, TJoined & Record<TJoinTable, TSchema[TJoinTable]>>,
365
- TRColumn extends AvailableColumns<TSchema, TTable, TOriginal, TJoined & Record<TJoinTable, TSchema[TJoinTable]>>,
389
+ TLColumn extends AvailableColumns<
390
+ TSchema,
391
+ TTable,
392
+ TOriginal,
393
+ TJoined & Record<TJoinTable, TSchema[TJoinTable]>
394
+ >,
395
+ TRColumn extends AvailableColumns<
396
+ TSchema,
397
+ TTable,
398
+ TOriginal,
399
+ TJoined & Record<TJoinTable, TSchema[TJoinTable]>
400
+ >,
366
401
  >(
367
402
  table: TJoinTable,
368
403
  left: TLColumn,
369
- right: TRColumn,
404
+ right: TRColumn
370
405
  ): Puri<
371
406
  TSchema,
372
407
  TTable,
@@ -407,7 +442,15 @@ export class Puri<
407
442
  }
408
443
 
409
444
  // OrderBy
410
- orderBy<TColumn extends ResultAvailableColumns<TSchema, TTable, TOriginal, TResult, TJoined>>(
445
+ orderBy<
446
+ TColumn extends ResultAvailableColumns<
447
+ TSchema,
448
+ TTable,
449
+ TOriginal,
450
+ TResult,
451
+ TJoined
452
+ >,
453
+ >(
411
454
  column: TColumn,
412
455
  direction: "asc" | "desc"
413
456
  ): Puri<TSchema, TTable, TOriginal, TResult, TJoined>;
@@ -431,21 +474,39 @@ export class Puri<
431
474
  }
432
475
 
433
476
  // Group by (조인된 테이블 컬럼도 지원)
434
- groupBy<TColumns extends ResultAvailableColumns<TSchema, TTable, TOriginal, TResult, TJoined>>(
435
- ...columns: TColumns[]
436
- ): Puri<TSchema, TTable, TOriginal, TResult, TJoined>;
437
- groupBy(...columns: string[]): Puri<TSchema, TTable, TOriginal, TResult, TJoined> {
477
+ groupBy<
478
+ TColumns extends ResultAvailableColumns<
479
+ TSchema,
480
+ TTable,
481
+ TOriginal,
482
+ TResult,
483
+ TJoined
484
+ >,
485
+ >(...columns: TColumns[]): Puri<TSchema, TTable, TOriginal, TResult, TJoined>;
486
+ groupBy(
487
+ ...columns: string[]
488
+ ): Puri<TSchema, TTable, TOriginal, TResult, TJoined> {
438
489
  this.knexQuery.groupBy(...(columns as string[]));
439
490
  return this;
440
491
  }
441
492
 
442
493
  having(condition: string): Puri<TSchema, TTable, TOriginal, TResult, TJoined>;
443
- having<TColumn extends ResultAvailableColumns<TSchema, TTable, TOriginal, TResult, TJoined>>(
494
+ having<
495
+ TColumn extends ResultAvailableColumns<
496
+ TSchema,
497
+ TTable,
498
+ TOriginal,
499
+ TResult,
500
+ TJoined
501
+ >,
502
+ >(
444
503
  condition: TColumn,
445
504
  operator: ComparisonOperator,
446
505
  value: any
447
506
  ): Puri<TSchema, TTable, TOriginal, TResult, TJoined>;
448
- having(...conditions: string[]): Puri<TSchema, TTable, TOriginal, TResult, TJoined> {
507
+ having(
508
+ ...conditions: string[]
509
+ ): Puri<TSchema, TTable, TOriginal, TResult, TJoined> {
449
510
  this.knexQuery.having(...(conditions as [string, string, string]));
450
511
  return this;
451
512
  }
@@ -641,6 +702,32 @@ export class Puri<
641
702
  raw(): Knex.QueryBuilder {
642
703
  return this.knexQuery;
643
704
  }
705
+
706
+ increment<
707
+ TColumn extends AvailableColumns<TSchema, TTable, TOriginal, TJoined>,
708
+ >(
709
+ column: TColumn,
710
+ value: number
711
+ ): Puri<TSchema, TTable, TOriginal, TResult, TJoined> {
712
+ if (value <= 0) {
713
+ throw new Error("Increment value must be greater than 0");
714
+ }
715
+ this.knexQuery.increment(column, value);
716
+ return this;
717
+ }
718
+
719
+ decrement<
720
+ TColumn extends AvailableColumns<TSchema, TTable, TOriginal, TJoined>,
721
+ >(
722
+ column: TColumn,
723
+ value: number
724
+ ): Puri<TSchema, TTable, TOriginal, TResult, TJoined> {
725
+ if (value <= 0) {
726
+ throw new Error("Decrement value must be greater than 0");
727
+ }
728
+ this.knexQuery.decrement(column, value);
729
+ return this;
730
+ }
644
731
  }
645
732
 
646
733
  // 11. Database 클래스
@@ -685,7 +772,9 @@ class WhereGroup<
685
772
  orWhere(
686
773
  conditions: WhereCondition<TSchema, TTable, TOriginal, TJoined>
687
774
  ): WhereGroup<TSchema, TTable, TOriginal, TJoined>;
688
- orWhere<TColumn extends AvailableColumns<TSchema, TTable, TOriginal, TJoined>>(
775
+ orWhere<
776
+ TColumn extends AvailableColumns<TSchema, TTable, TOriginal, TJoined>,
777
+ >(
689
778
  column: TColumn,
690
779
  value: ExtractColumnType<
691
780
  TSchema,
@@ -695,7 +784,9 @@ class WhereGroup<
695
784
  TJoined
696
785
  >
697
786
  ): WhereGroup<TSchema, TTable, TOriginal, TJoined>;
698
- orWhere<TColumn extends AvailableColumns<TSchema, TTable, TOriginal, TJoined>>(
787
+ orWhere<
788
+ TColumn extends AvailableColumns<TSchema, TTable, TOriginal, TJoined>,
789
+ >(
699
790
  column: TColumn,
700
791
  operator: ComparisonOperator | "like",
701
792
  value: ExtractColumnType<
@@ -712,7 +803,9 @@ class WhereGroup<
712
803
  return this;
713
804
  }
714
805
 
715
- whereIn<TColumn extends AvailableColumns<TSchema, TTable, TOriginal, TJoined>>(
806
+ whereIn<
807
+ TColumn extends AvailableColumns<TSchema, TTable, TOriginal, TJoined>,
808
+ >(
716
809
  column: TColumn,
717
810
  values: ExtractColumnType<
718
811
  TSchema,
@@ -789,18 +882,28 @@ export class JoinClauseGroup<
789
882
  constructor(private callback: Knex.JoinClause) {}
790
883
 
791
884
  on(
792
- callback: (joinClause: JoinClauseGroup<TSchema, TTable, TOriginal, TJoined>) => void
885
+ callback: (
886
+ joinClause: JoinClauseGroup<TSchema, TTable, TOriginal, TJoined>
887
+ ) => void
888
+ ): JoinClauseGroup<TSchema, TTable, TOriginal, TJoined>;
889
+ on(
890
+ column: string,
891
+ value: any
793
892
  ): JoinClauseGroup<TSchema, TTable, TOriginal, TJoined>;
794
- on(column: string, value: any): JoinClauseGroup<TSchema, TTable, TOriginal, TJoined>;
795
893
  on(...args: any[]): JoinClauseGroup<TSchema, TTable, TOriginal, TJoined> {
796
894
  this.callback.on(...(args as [string, string]));
797
895
  return this;
798
896
  }
799
897
 
800
898
  orOn(
801
- callback: (joinClause: JoinClauseGroup<TSchema, TTable, TOriginal, TJoined>) => void
899
+ callback: (
900
+ joinClause: JoinClauseGroup<TSchema, TTable, TOriginal, TJoined>
901
+ ) => void
902
+ ): JoinClauseGroup<TSchema, TTable, TOriginal, TJoined>;
903
+ orOn(
904
+ column: string,
905
+ value: any
802
906
  ): JoinClauseGroup<TSchema, TTable, TOriginal, TJoined>;
803
- orOn(column: string, value: any): JoinClauseGroup<TSchema, TTable, TOriginal, TJoined>;
804
907
  orOn(...args: any[]): JoinClauseGroup<TSchema, TTable, TOriginal, TJoined> {
805
908
  this.callback.orOn(...(args as [string, string]));
806
909
  return this;
@@ -1,20 +1,33 @@
1
1
  export type ComparisonOperator = "=" | ">" | ">=" | "<" | "<=" | "<>" | "!=";
2
2
  export type Expand<T> = T extends any[]
3
3
  ? { [K in keyof T[0]]: T[0][K] }[] // 배열이면 첫 번째 요소를 Expand하고 배열로 감쌈
4
- : T extends object ? { [K in keyof T]: T[K] } : T;
5
-
4
+ : T extends object
5
+ ? { [K in keyof T]: T[K] }
6
+ : T;
7
+
6
8
  // EmptyRecord가 남아있으면 AvailableColumns 추론이 제대로 되지 않음 (EmptyRecord를 {}로 변경하면 정상 동작함)
7
- export type MergeJoined<TExisting, TNew> =
8
- TExisting extends EmptyRecord
9
- ? TNew // join: EmptyRecord 제거하고 대체
10
- : TExisting & TNew; // 이후 join: 누적
9
+ export type MergeJoined<TExisting, TNew> = TExisting extends EmptyRecord
10
+ ? TNew // 첫 join: EmptyRecord 제거하고 대체
11
+ : TExisting & TNew; // 이후 join: 누적
11
12
 
12
- type DeepEqual<T, U> = [T] extends [U] ? [U] extends [T] ? true : false : false;
13
- type Extends<T, U> = DeepEqual<T, Record<string, never>> extends true ? false : (T extends U ? true : false);
13
+ type DeepEqual<T, U> = [T] extends [U]
14
+ ? [U] extends [T]
15
+ ? true
16
+ : false
17
+ : false;
18
+ type Extends<T, U> =
19
+ DeepEqual<T, Record<string, never>> extends true
20
+ ? false
21
+ : T extends U
22
+ ? true
23
+ : false;
14
24
  type NullableToOptional<T> = {
15
- [K in keyof T as T[K] extends null | undefined ? K : never]?: Exclude<T[K], null | undefined>
25
+ [K in keyof T as T[K] extends null | undefined ? K : never]?: Exclude<
26
+ T[K],
27
+ null | undefined
28
+ >;
16
29
  } & Partial<{
17
- [K in keyof T as T[K] extends null | undefined ? never : K]: T[K]
30
+ [K in keyof T as T[K] extends null | undefined ? never : K]: T[K];
18
31
  }>;
19
32
 
20
33
  // Join 등이 Empty 상태일 떄 {}가 아니라 EmptyRecord를 써서
@@ -27,7 +40,9 @@ export type ResultAvailableColumns<
27
40
  TOriginal = any,
28
41
  TResult = any,
29
42
  TJoined = EmptyRecord,
30
- > = AvailableColumns<TSchema, T, TOriginal, TJoined> | `${keyof TResult & string}`;
43
+ > =
44
+ | AvailableColumns<TSchema, T, TOriginal, TJoined>
45
+ | `${keyof TResult & string}`;
31
46
 
32
47
  // 사용 가능한 컬럼 경로 타입 (메인 테이블 + 조인된 테이블들)
33
48
  export type AvailableColumns<
@@ -37,14 +52,14 @@ export type AvailableColumns<
37
52
  TJoined = EmptyRecord,
38
53
  > = T extends keyof TSchema
39
54
  ? // 기존 테이블 케이스
40
- | (Extends<TJoined, Record<string, any>> extends false
41
- // 이게 TSchema[T]에 존재하면
42
- ? keyof TSchema[T]
43
- : {
44
- [K in keyof TJoined]: TJoined[K] extends Record<string, any>
45
- ? `${string & K}.${keyof TJoined[K] & string}`
46
- : never;
47
- }[keyof TJoined])
55
+ | (Extends<TJoined, Record<string, any>> extends false
56
+ ? // 이게 TSchema[T]에 존재하면
57
+ keyof TSchema[T]
58
+ : {
59
+ [K in keyof TJoined]: TJoined[K] extends Record<string, any>
60
+ ? `${string & K}.${keyof TJoined[K] & string}`
61
+ : never;
62
+ }[keyof TJoined])
48
63
  | `${T & string}.${keyof TSchema[T] & string}`
49
64
  : // 서브쿼리 케이스 (T는 alias string)
50
65
  | keyof TOriginal
@@ -156,27 +171,15 @@ export type WhereCondition<
156
171
  T extends keyof TSchema | string,
157
172
  TOriginal = any,
158
173
  TJoined = EmptyRecord,
159
- > =
160
- // 메인 테이블/서브쿼리 조건들
161
- (T extends keyof TSchema
162
- ? {
163
- [K in keyof TSchema[T]]?: TSchema[T][K] | TSchema[T][K][];
164
- }
165
- : {
166
- [K in keyof TOriginal]?: TOriginal[K] | TOriginal[K][];
167
- }) &
168
- // 조인된 테이블들의 조건들
169
- (TJoined extends Record<string, any>
170
- ? {
171
- [K in keyof TJoined as TJoined[K] extends Record<string, any>
172
- ? keyof TJoined[K] & string
173
- : never]?: TJoined[K] extends Record<string, any>
174
- ?
175
- | TJoined[K][K extends keyof TJoined[K] ? K : never]
176
- | TJoined[K][K extends keyof TJoined[K] ? K : never][]
177
- : never;
178
- }
179
- : Record<string, never>);
174
+ > = {
175
+ [key in AvailableColumns<TSchema, T, TOriginal, TJoined>]?: ExtractColumnType<
176
+ TSchema,
177
+ T,
178
+ key & string,
179
+ TOriginal,
180
+ TJoined
181
+ >;
182
+ };
180
183
 
181
184
  // Fulltext index 컬럼 추출 타입 (메인 테이블 + 조인된 테이블)
182
185
  export type FulltextColumns<
@@ -219,4 +222,6 @@ export type FulltextColumns<
219
222
  : never);
220
223
 
221
224
  // Insert 타입: id, created_at 제외
222
- export type InsertData<T> = NullableToOptional<Omit<T, "id" | "created_at" | "__fulltext__">>;
225
+ export type InsertData<T> = NullableToOptional<
226
+ Omit<T, "id" | "created_at" | "__fulltext__">
227
+ >;
package/src/index.ts CHANGED
@@ -16,9 +16,10 @@ export * from "./utils/controller";
16
16
  export * from "./utils/model";
17
17
  export * from "./utils/utils";
18
18
  export * from "./testing/fixture-manager";
19
- export * from "./migration/migrator";
20
19
  export * from "./entity/entity-manager";
21
20
  export * from "./entity/entity";
21
+ export * from "./migration/migrator";
22
+ export * from "./migration/types";
22
23
  export * from "./file-storage/driver";
23
24
 
24
25
  // export * from "./api/code-converters";
@@ -77,7 +77,7 @@ function genColumnDefinitions(columns: MigrationColumn[]): string[] {
77
77
  columnType = "text";
78
78
  }
79
79
  chains.push(
80
- `${column.type}('${column.name}'${
80
+ `${columnType}('${column.name}'${
81
81
  column.length ? `, ${column.length}` : ""
82
82
  }${extraType ? `, '${extraType}'` : ""})`
83
83
  );
@@ -680,9 +680,9 @@ export async function generateAlterCode(
680
680
  // boolean인 경우 기본값 정규화 (MySQL에서는 TINYINT(1)로 저장되므로 0 또는 1로 정규화)
681
681
  // TODO: db.ts에 typeCase 설정 확인하여 처리하도록 수정 필요
682
682
  if (col.type === "boolean" && col.defaultTo !== undefined) {
683
- if (col.defaultTo === "0" || col.defaultTo === "false") {
683
+ if (col.defaultTo === "0" || col.defaultTo.toLowerCase() === "false") {
684
684
  col.defaultTo = "0";
685
- } else if (col.defaultTo === "1" || col.defaultTo === "true") {
685
+ } else if (col.defaultTo === "1" || col.defaultTo.toLowerCase() === "true") {
686
686
  col.defaultTo = "1";
687
687
  }
688
688
  }
@@ -1,3 +1,4 @@
1
+ import { SonamuDBConfig } from "../database/db";
1
2
  import { GenMigrationCode } from "../types/types";
2
3
 
3
4
  export type MigrationCode = {
@@ -10,7 +11,7 @@ export type MigrationStatus = {
10
11
  codes: MigrationCode[];
11
12
  conns: {
12
13
  name: string;
13
- connKey: string;
14
+ connKey: keyof SonamuDBConfig;
14
15
  connString: ConnString;
15
16
  currentVersion: string;
16
17
  status: string | number;
@@ -84,7 +84,12 @@ export class Template__generated_http extends Template {
84
84
  ])
85
85
  );
86
86
  } else if (zodType instanceof z.ZodArray) {
87
- return [this.zodTypeToReqDefault((zodType as z.ZodArray<z.ZodType>).element, name)];
87
+ return [
88
+ this.zodTypeToReqDefault(
89
+ (zodType as z.ZodArray<z.ZodType>).element,
90
+ name
91
+ ),
92
+ ];
88
93
  } else if (zodType instanceof z.ZodString) {
89
94
  if (name.endsWith("_at") || name.endsWith("_date") || name === "range") {
90
95
  return "2000-01-01";
@@ -97,17 +102,23 @@ export class Template__generated_http extends Template {
97
102
  }
98
103
 
99
104
  const minValue = zodType.minValue ?? 0;
100
- return minValue > Number.MIN_SAFE_INTEGER ? minValue : 0;
105
+ return minValue > Number.MIN_SAFE_INTEGER ? minValue : 0;
101
106
  } else if (zodType instanceof z.ZodBoolean) {
102
107
  return false;
103
108
  } else if (zodType instanceof z.ZodEnum) {
104
109
  return zodType.options[0];
105
110
  } else if (zodType instanceof z.ZodOptional) {
106
- return this.zodTypeToReqDefault((zodType as z.ZodOptional<z.ZodType>).def.innerType, name);
111
+ return this.zodTypeToReqDefault(
112
+ (zodType as z.ZodOptional<z.ZodType>).def.innerType,
113
+ name
114
+ );
107
115
  } else if (zodType instanceof z.ZodNullable) {
108
116
  return null;
109
117
  } else if (zodType instanceof z.ZodUnion) {
110
- return this.zodTypeToReqDefault((zodType as z.ZodUnion<z.ZodType[]>).def.options[0], name);
118
+ return this.zodTypeToReqDefault(
119
+ (zodType as z.ZodUnion<z.ZodType[]>).def.options[0],
120
+ name
121
+ );
111
122
  } else if (zodType instanceof z.ZodUnknown) {
112
123
  return "unknown";
113
124
  } else if (zodType instanceof z.ZodTuple) {
@@ -119,16 +130,29 @@ export class Template__generated_http extends Template {
119
130
  } else if (zodType instanceof z.ZodLiteral) {
120
131
  return zodType.value;
121
132
  } else if (zodType instanceof z.ZodRecord || zodType instanceof z.ZodMap) {
122
- const kvDef = (zodType as z.ZodRecord<any, z.ZodType> | z.ZodMap<z.ZodType, z.ZodType>).def
133
+ const kvDef = (
134
+ zodType as z.ZodRecord<any, z.ZodType> | z.ZodMap<z.ZodType, z.ZodType>
135
+ ).def;
123
136
  const key = this.zodTypeToReqDefault(kvDef.keyType, name) as any;
124
137
  const value = this.zodTypeToReqDefault(kvDef.valueType, name);
125
138
  return { [key]: value };
126
139
  } else if (zodType instanceof z.ZodSet) {
127
- return [this.zodTypeToReqDefault((zodType as z.ZodSet<z.ZodType>).def.valueType, name)];
140
+ return [
141
+ this.zodTypeToReqDefault(
142
+ (zodType as z.ZodSet<z.ZodType>).def.valueType,
143
+ name
144
+ ),
145
+ ];
128
146
  } else if (zodType instanceof z.ZodIntersection) {
129
- return this.zodTypeToReqDefault((zodType as z.ZodIntersection<z.ZodType, z.ZodType>).def.right, name);
147
+ return this.zodTypeToReqDefault(
148
+ (zodType as z.ZodIntersection<z.ZodType, z.ZodType>).def.right,
149
+ name
150
+ );
130
151
  } else if (zodType instanceof z.ZodDefault) {
131
- return this.zodTypeToReqDefault((zodType as z.ZodDefault<z.ZodType>).def.innerType, name);
152
+ return this.zodTypeToReqDefault(
153
+ (zodType as z.ZodDefault<z.ZodType>).def.innerType,
154
+ name
155
+ );
132
156
  } else {
133
157
  // console.log(zodType);
134
158
  return `unknown-${zodType.type}`;
@@ -140,8 +164,15 @@ export class Template__generated_http extends Template {
140
164
  references: { [typeName: string]: z.ZodObject<any> }
141
165
  ): { [key: string]: unknown } {
142
166
  const reqType = getZodObjectFromApi(api, references);
143
- return this.zodTypeToReqDefault(reqType, "unknownName") as {
144
- [key: string]: unknown;
145
- };
167
+ try {
168
+ const def = this.zodTypeToReqDefault(reqType, "unknownName") as {
169
+ [key: string]: unknown;
170
+ };
171
+ return def;
172
+ } catch (error) {
173
+ throw new Error(
174
+ `Invalid zod type detected on ${api.modelName}:${api.methodName}`
175
+ );
176
+ }
146
177
  }
147
178
  }
@@ -89,12 +89,14 @@ export class Template__generated_sso extends Template {
89
89
  } as Omit<SourceCode, "label">
90
90
  );
91
91
 
92
+ const body = sourceCode.lines.join("\n");
93
+ const isUsingManyToManyBaseSchema = body.includes("ManyToManyBaseSchema");
92
94
  return {
93
95
  ...this.getTargetAndPath(),
94
96
  body: sourceCode.lines.join("\n"),
95
97
  importKeys: sourceCode.importKeys,
96
98
  customHeaders: [
97
- `import { SubsetQuery, ManyToManyBaseSchema } from "sonamu";`,
99
+ `import { SubsetQuery, ${isUsingManyToManyBaseSchema ? "ManyToManyBaseSchema" : ""} } from "sonamu";`,
98
100
  ],
99
101
  };
100
102
  }
@@ -219,18 +219,31 @@ export async function ${methodNameAxios}${typeParamsDef}(${paramsDef}): Promise<
219
219
  returnTypeDef: string,
220
220
  paramsWithoutContext: ApiParam[]
221
221
  ) {
222
- const formDataDef = [
223
- 'formData.append("file", file);',
224
- ...paramsWithoutContext.map(
225
- (param) => `formData.append('${param.name}', String(${param.name}));`
226
- ),
227
- ].join("\n");
222
+ const isMultiple = api.uploadOptions?.mode === "multiple";
223
+ const fileParamName = isMultiple ? "files" : "file";
224
+ const fileParamType = isMultiple ? "File[]" : "File";
225
+
226
+ const formDataDef = isMultiple
227
+ ? [
228
+ `${fileParamName}.forEach(f => formData.append("${fileParamName}", f));`,
229
+ ...paramsWithoutContext.map(
230
+ (param) =>
231
+ `formData.append('${param.name}', String(${param.name}));`
232
+ ),
233
+ ].join("\n")
234
+ : [
235
+ `formData.append("${fileParamName}", ${fileParamName});`,
236
+ ...paramsWithoutContext.map(
237
+ (param) =>
238
+ `formData.append('${param.name}', String(${param.name}));`
239
+ ),
240
+ ].join("\n");
228
241
 
229
242
  const paramsDefComma = paramsDef !== "" ? ", " : "";
230
243
  return `
231
244
  export async function ${api.methodName}${typeParamsDef}(
232
245
  ${paramsDef}${paramsDefComma}
233
- file: File,
246
+ ${fileParamName}: ${fileParamType},
234
247
  onUploadProgress?: (pe:AxiosProgressEvent) => void
235
248
  ): Promise<${returnTypeDef}> {
236
249
  const formData = new FormData();
@@ -568,6 +568,8 @@ export class FixtureManagerClass {
568
568
  if (!isRelationProp(prop)) {
569
569
  if (prop.type === "json") {
570
570
  insertData[propName] = JSON.stringify(column.value);
571
+ } else if (prop.type === "timestamp" || prop.type === "datetime") {
572
+ insertData[propName] = new Date(column.value);
571
573
  } else {
572
574
  insertData[propName] = column.value;
573
575
  }
package/.swcrc DELETED
@@ -1,15 +0,0 @@
1
- {
2
- "$schema": "https://swc.rs/schema.json",
3
- "module": {
4
- "type": "commonjs"
5
- },
6
- "jsc": {
7
- "parser": {
8
- "syntax": "typescript",
9
- "decorators": true
10
- },
11
- "target": "es5"
12
- },
13
- "minify": true,
14
- "sourceMaps": true
15
- }
@@ -1,27 +0,0 @@
1
- const fs = require("fs/promises");
2
-
3
- export const ImportToRequirePlugin = {
4
- name: "import-to-require",
5
- setup(build) {
6
- if (build.initialOptions.define.TSUP_FORMAT === '"cjs"') {
7
- // 빌드 전에 src/database/db.ts 파일을 읽어서 변환
8
- build.onLoad({ filter: /database\/db.ts/ }, async (args) => {
9
- console.debug(`reading ${args.path}`);
10
- let contents = await fs.readFile(args.path, "utf8");
11
-
12
- // 'await import(' 패턴을 찾아 'require('로 변환
13
- contents = contents.replace(
14
- /\bawait import\(([^)]+)\)/g,
15
- (_, modulePath) => {
16
- return `require(${modulePath})`;
17
- }
18
- );
19
-
20
- return {
21
- contents,
22
- loader: "ts", // TypeScript를 지원하도록 'ts' 로더 설정
23
- };
24
- });
25
- }
26
- },
27
- };
package/nodemon.json DELETED
@@ -1,6 +0,0 @@
1
- {
2
- "watch": ["src"],
3
- "ext": "ts,json",
4
- "exec": "swc src --config-file .swcrc -d dist --strip-leading-paths && tsc --emitDeclarationOnly && node -r source-map-support/register -r dotenv/config dist/index.js",
5
- "signal": "SIGUSR2"
6
- }
package/tsconfig.json DELETED
@@ -1,56 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- /* Basic Options */
4
- "target": "ESNext",
5
- "module": "ESNext",
6
- "outDir": "dist",
7
- "sourceMap": true,
8
- "lib": ["esnext", "dom"],
9
-
10
- // NOTE(Haze, 251106): SSE 관련 fastify 타입 이슈로 명시적으로 추가함.
11
- "types": ["fastify-sse-v2"],
12
-
13
- "declaration": true,
14
- "declarationMap": true,
15
-
16
- /* Strict Type-Checking Options */
17
- "strict": true,
18
- "noImplicitAny": true,
19
- "strictNullChecks": true,
20
- "strictFunctionTypes": true,
21
- "strictBindCallApply": true,
22
- "strictPropertyInitialization": true,
23
- "noImplicitThis": true,
24
- "alwaysStrict": true,
25
-
26
- /* Additional Checks */
27
- "noUnusedLocals": true,
28
- "noUnusedParameters": true,
29
- "noImplicitReturns": true,
30
- "noFallthroughCasesInSwitch": true,
31
- "skipLibCheck": true,
32
- // "noUncheckedIndexedAccess": true, // FIXME
33
-
34
- /* Module Resolution Options */
35
- "moduleResolution": "node",
36
- "esModuleInterop": true,
37
-
38
- /* Experimental Options */
39
- "experimentalDecorators": true,
40
- "emitDecoratorMetadata": true,
41
-
42
- /* Advanced Options */
43
- "forceConsistentCasingInFileNames": true,
44
- "noErrorTruncation": true
45
- },
46
- "exclude": [
47
- "node_modules",
48
- "dist",
49
- "src/**/*.test.ts",
50
- "src/**/*.test-hold.ts",
51
- "src/**/*.ignore.ts",
52
- "wasted_src/**",
53
- "_templates/**",
54
- "**/__mocks__/**"
55
- ]
56
- }
package/tsup.config.js DELETED
@@ -1,47 +0,0 @@
1
- // tsup.config.js
2
- import { defineConfig } from "tsup";
3
- import { ImportToRequirePlugin } from "./import-to-require";
4
-
5
- export default defineConfig({
6
- entry: [
7
- "src/index.ts",
8
- "src/bin/cli.ts",
9
- "src/bin/cli-wrapper.ts",
10
- "src/database/drivers/knex/base-model.ts",
11
- "src/database/drivers/kysely/base-model.ts",
12
- ],
13
- dts: true,
14
- format: [
15
- "cjs",
16
- // "esm"
17
- ],
18
- target: "esnext",
19
- clean: true,
20
- sourcemap: true,
21
- shims: true,
22
- platform: "node",
23
- splitting: true,
24
- esbuildPlugins: [ImportToRequirePlugin],
25
- external: [
26
- "chalk",
27
- "dotenv",
28
- "fast-deep-equal",
29
- "fastify",
30
- "glob",
31
- "inflection",
32
- "knex",
33
- "lodash",
34
- "luxon",
35
- "mysql2",
36
- "node-sql-parser",
37
- "prompts",
38
- "qs",
39
- "tsicli",
40
- "uuid",
41
- "zod",
42
- "prettier",
43
- "source-map-support",
44
- "tsup",
45
- "typescript",
46
- ],
47
- });