@gencow/core 0.1.13 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -26,7 +26,7 @@ import { pgTable, serial, text, timestamp } from "drizzle-orm/pg-core";
26
26
 
27
27
  // ─── crud import ───────────────────────────────────────────────────────
28
28
 
29
- import { crud } from "../crud";
29
+ import { crud, parseFilterNode, applyFilterOp } from "../crud";
30
30
 
31
31
  // ─── 테스트 테이블 정의 ────────────────────────────────────────────────────
32
32
 
@@ -525,3 +525,423 @@ describe("crud() — allowedFilters", () => {
525
525
  expect(result2).toHaveProperty("data");
526
526
  });
527
527
  });
528
+
529
+ // ═══════════════════════════════════════════════════════════════════════════════
530
+ // 11. v3 Filter Engine — parseFilterNode 직접 테스트 (spec TC1~5)
531
+ // ═══════════════════════════════════════════════════════════════════════════════
532
+
533
+ describe("v3 Filter Engine — parseFilterNode", () => {
534
+ // TC1: 하위 호환성 (Implicit eq)
535
+ it("TC1: 단순 key-value는 암묵적 eq로 파싱된다 (하위호환)", () => {
536
+ const table = pgTable("tc1_test", {
537
+ id: serial("id").primaryKey(),
538
+ status: text("status"),
539
+ });
540
+
541
+ const result = parseFilterNode({ status: "active" }, table);
542
+ expect(result).toBeDefined();
543
+ });
544
+
545
+ it("TC1: 여러 key-value 동시 사용 시 모두 AND 결합된다", () => {
546
+ const table = pgTable("tc1b_test", {
547
+ id: serial("id").primaryKey(),
548
+ status: text("status"),
549
+ category: text("category"),
550
+ });
551
+
552
+ const result = parseFilterNode({ status: "active", category: "tech" }, table);
553
+ expect(result).toBeDefined();
554
+ });
555
+
556
+ // TC2: 다중 논리망 (Nested OR & AND)
557
+ it("TC2: AND + 중첩 OR 필터가 SQL 조건으로 빌드된다", () => {
558
+ const table = pgTable("tc2_test", {
559
+ id: serial("id").primaryKey(),
560
+ category: text("category"),
561
+ qty: serial("qty"),
562
+ flag: text("flag"),
563
+ });
564
+
565
+ const result = parseFilterNode({
566
+ AND: [
567
+ { category: "A" },
568
+ { OR: [
569
+ { qty: { op: "gte", value: 10 } },
570
+ { flag: true },
571
+ ] },
572
+ ],
573
+ }, table);
574
+
575
+ expect(result).toBeDefined();
576
+ });
577
+
578
+ it("TC2: 단순 OR 배열도 파싱된다", () => {
579
+ const table = pgTable("tc2b_test", {
580
+ id: serial("id").primaryKey(),
581
+ title: text("title"),
582
+ description: text("description"),
583
+ });
584
+
585
+ const result = parseFilterNode({
586
+ OR: [
587
+ { title: { op: "ilike", value: "%AI%" } },
588
+ { description: { op: "ilike", value: "%데이터%" } },
589
+ ],
590
+ }, table);
591
+
592
+ expect(result).toBeDefined();
593
+ });
594
+
595
+ // TC3: 보안 방어 — allowedFilters 위반
596
+ it("TC3: allowedFilters에 없는 필드는 완전 묵살 (직접 접근)", () => {
597
+ const table = pgTable("tc3_test", {
598
+ id: serial("id").primaryKey(),
599
+ status: text("status"),
600
+ title: text("title"),
601
+ passwordHash: text("password_hash"),
602
+ });
603
+
604
+ // password_hash 직접 접근 → 묵살
605
+ const r1 = parseFilterNode(
606
+ { passwordHash: { op: "eq", value: "xxx" } },
607
+ table,
608
+ ["status", "title"],
609
+ );
610
+ expect(r1).toBeUndefined(); // 모든 조건 제거됨
611
+ });
612
+
613
+ it("TC3: OR 틈새 우회 시도 시 미허용 필드만 묵살, 허용 필드는 유지", () => {
614
+ const table = pgTable("tc3b_test", {
615
+ id: serial("id").primaryKey(),
616
+ status: text("status"),
617
+ isAdmin: text("is_admin"),
618
+ });
619
+
620
+ const r = parseFilterNode(
621
+ { OR: [{ status: "public" }, { isAdmin: true }] },
622
+ table,
623
+ ["status"],
624
+ );
625
+ // status는 허용 → OR 조건 살아있음
626
+ expect(r).toBeDefined();
627
+ });
628
+
629
+ it("TC3: 모든 필드가 미허용이면 undefined 반환", () => {
630
+ const table = pgTable("tc3c_test", {
631
+ id: serial("id").primaryKey(),
632
+ secret: text("secret"),
633
+ });
634
+
635
+ const r = parseFilterNode(
636
+ { OR: [{ secret: "hi" }] },
637
+ table,
638
+ ["status"],
639
+ );
640
+ expect(r).toBeUndefined();
641
+ });
642
+
643
+ // TC4: 악의적 페이로드 구조 방어
644
+ it("TC4: OR에 비배열 전달 시 무시, 크래시 없음", () => {
645
+ const table = pgTable("tc4_test", {
646
+ id: serial("id").primaryKey(),
647
+ age: serial("age"),
648
+ });
649
+
650
+ expect(parseFilterNode({ OR: "not-an-array" as any }, table)).toBeUndefined();
651
+ });
652
+
653
+ it("TC4: 미지원 op 키워드(DROP TABLE 등) 묵살, 크래시 없음", () => {
654
+ const table = pgTable("tc4b_test", {
655
+ id: serial("id").primaryKey(),
656
+ age: serial("age"),
657
+ });
658
+
659
+ expect(parseFilterNode({ age: { op: "DROP TABLE", value: 1 } }, table)).toBeUndefined();
660
+ });
661
+
662
+ it("TC4: AND에 null/string/number 등 비객체 원소 묵살", () => {
663
+ const table = pgTable("tc4c_test", {
664
+ id: serial("id").primaryKey(),
665
+ });
666
+
667
+ expect(parseFilterNode({ AND: [null, "string", 42] as any }, table)).toBeUndefined();
668
+ expect(parseFilterNode({ AND: null as any }, table)).toBeUndefined();
669
+ });
670
+
671
+ it("TC4: 존재하지 않는 컬럼명 필터는 묵살", () => {
672
+ const table = pgTable("tc4d_test", {
673
+ id: serial("id").primaryKey(),
674
+ name: text("name"),
675
+ });
676
+
677
+ const r = parseFilterNode({ nonExistentCol: "value" }, table);
678
+ expect(r).toBeUndefined();
679
+ });
680
+
681
+ // TC5: IN / NIN 연산자 배열값 처리
682
+ it("TC5: in 연산자 — 정상 배열은 SQL 생성", () => {
683
+ const table = pgTable("tc5_test", {
684
+ id: serial("id").primaryKey(),
685
+ status: text("status"),
686
+ });
687
+
688
+ const r = parseFilterNode({ id: { op: "in", value: [1, 2, 3] } }, table);
689
+ expect(r).toBeDefined();
690
+ });
691
+
692
+ it("TC5: in 연산자 — 비배열은 묵살", () => {
693
+ const table = pgTable("tc5b_test", {
694
+ id: serial("id").primaryKey(),
695
+ });
696
+
697
+ const r = parseFilterNode({ id: { op: "in", value: "1" } }, table);
698
+ expect(r).toBeUndefined();
699
+ });
700
+
701
+ it("TC5: in 연산자 — 빈 배열은 묵살", () => {
702
+ const table = pgTable("tc5c_test", {
703
+ id: serial("id").primaryKey(),
704
+ });
705
+
706
+ const r = parseFilterNode({ id: { op: "in", value: [] } }, table);
707
+ expect(r).toBeUndefined();
708
+ });
709
+
710
+ it("TC5: nin 연산자 — 정상 배열은 SQL 생성", () => {
711
+ const table = pgTable("tc5d_test", {
712
+ id: serial("id").primaryKey(),
713
+ });
714
+
715
+ const r = parseFilterNode({ id: { op: "nin", value: [10, 20] } }, table);
716
+ expect(r).toBeDefined();
717
+ });
718
+ });
719
+
720
+ // ═══════════════════════════════════════════════════════════════════════════════
721
+ // 12. v3 Filter Engine — applyFilterOp 직접 테스트
722
+ // ═══════════════════════════════════════════════════════════════════════════════
723
+
724
+ describe("v3 Filter Engine — applyFilterOp", () => {
725
+ const table = pgTable("op_test", {
726
+ id: serial("id").primaryKey(),
727
+ name: text("name"),
728
+ age: serial("age"),
729
+ });
730
+
731
+ it("eq 연산자가 SQL 조건을 반환한다", () => {
732
+ expect(applyFilterOp(table.name, "eq", "test")).toBeDefined();
733
+ });
734
+
735
+ it("ne 연산자가 SQL 조건을 반환한다", () => {
736
+ expect(applyFilterOp(table.name, "ne", "test")).toBeDefined();
737
+ });
738
+
739
+ it("gt/gte/lt/lte 비교 연산자가 SQL 조건을 반환한다", () => {
740
+ expect(applyFilterOp(table.age, "gt", 10)).toBeDefined();
741
+ expect(applyFilterOp(table.age, "gte", 18)).toBeDefined();
742
+ expect(applyFilterOp(table.age, "lt", 100)).toBeDefined();
743
+ expect(applyFilterOp(table.age, "lte", 65)).toBeDefined();
744
+ });
745
+
746
+ it("like/ilike 패턴 연산자가 SQL 조건을 반환한다", () => {
747
+ expect(applyFilterOp(table.name, "like", "%test%")).toBeDefined();
748
+ expect(applyFilterOp(table.name, "ilike", "%TEST%")).toBeDefined();
749
+ });
750
+
751
+ it("in 연산자 — 정상 배열은 SQL 반환", () => {
752
+ expect(applyFilterOp(table.id, "in", [1, 2, 3])).toBeDefined();
753
+ });
754
+
755
+ it("nin 연산자 — 빈 배열은 undefined 반환", () => {
756
+ expect(applyFilterOp(table.id, "nin", [])).toBeUndefined();
757
+ });
758
+
759
+ it("미지원 연산자는 undefined 반환", () => {
760
+ expect(applyFilterOp(table.id, "INVALID" as any, 1)).toBeUndefined();
761
+ });
762
+ });
763
+
764
+ // ═══════════════════════════════════════════════════════════════════════════════
765
+ // 13. v3 crud() handler 통합 테스트 — Advanced Filters
766
+ // ═══════════════════════════════════════════════════════════════════════════════
767
+
768
+ describe("v3 crud() — advanced filters through handler", () => {
769
+ it("allowedFilters 미설정 시 필터가 무시된다 (Secure by Default)", async () => {
770
+ const openTable = pgTable("v3_open_filter", {
771
+ id: serial("id").primaryKey(),
772
+ status: text("status"),
773
+ createdAt: timestamp("created_at").defaultNow(),
774
+ });
775
+
776
+ // allowedFilters 미설정 → 필터 전체 무시 (v2 호환, Secure by Default)
777
+ crud(openTable, { public: true });
778
+
779
+ const listDef = getQueryDef("v3_open_filter.list");
780
+ expect(listDef).toBeDefined();
781
+
782
+ const mockCtx = createListMockCtx([{ id: 1, status: "active" }]);
783
+ const result = await listDef!.handler(mockCtx, {
784
+ filters: { status: { op: "eq", value: "active" } },
785
+ });
786
+
787
+ // 필터가 무시되어도 에러 없이 정상 동작
788
+ expect(result).toHaveProperty("data");
789
+ expect(result).toHaveProperty("total");
790
+ });
791
+
792
+ it("allowedFilters 설정 시 v3 고급 필터가 동작한다", async () => {
793
+ const advTable = pgTable("v3_adv_filter", {
794
+ id: serial("id").primaryKey(),
795
+ status: text("status"),
796
+ createdAt: timestamp("created_at").defaultNow(),
797
+ });
798
+
799
+ crud(advTable, { public: true, allowedFilters: ["status"] });
800
+
801
+ const listDef = getQueryDef("v3_adv_filter.list");
802
+ const mockCtx = createListMockCtx([{ id: 1, status: "active" }]);
803
+ const result = await listDef!.handler(mockCtx, {
804
+ filters: { status: { op: "eq", value: "active" } },
805
+ });
806
+
807
+ expect(result).toHaveProperty("data");
808
+ expect(result).toHaveProperty("total");
809
+ });
810
+
811
+ it("복합 OR/AND 필터가 handler에서 에러 없이 실행된다", async () => {
812
+ const complexTable = pgTable("v3_complex", {
813
+ id: serial("id").primaryKey(),
814
+ title: text("title"),
815
+ status: text("status"),
816
+ createdAt: timestamp("created_at").defaultNow(),
817
+ });
818
+
819
+ crud(complexTable, { public: true, allowedFilters: ["title", "status"] });
820
+
821
+ const listDef = getQueryDef("v3_complex.list");
822
+ const mockCtx = createListMockCtx([]);
823
+
824
+ const result = await listDef!.handler(mockCtx, {
825
+ filters: {
826
+ OR: [
827
+ { title: { op: "ilike", value: "%AI%" } },
828
+ { status: { op: "in", value: ["active", "archived"] } },
829
+ ],
830
+ },
831
+ });
832
+
833
+ expect(result).toHaveProperty("data");
834
+ expect(result.total).toBe(0);
835
+ });
836
+
837
+ it("악성 페이로드가 handler를 크래시시키지 않는다", async () => {
838
+ const safeTable = pgTable("v3_safe", {
839
+ id: serial("id").primaryKey(),
840
+ name: text("name"),
841
+ createdAt: timestamp("created_at").defaultNow(),
842
+ });
843
+
844
+ crud(safeTable, { public: true });
845
+
846
+ const listDef = getQueryDef("v3_safe.list");
847
+ const mockCtx = createListMockCtx([{ id: 1 }]);
848
+
849
+ // 다양한 악성 페이로드 — 모두 에러 없이 실행
850
+ const payloads = [
851
+ { filters: { OR: "not-array" } },
852
+ { filters: { AND: null } },
853
+ { filters: { name: { op: "EVIL_OP", value: 1 } } },
854
+ { filters: { OR: [null, undefined, 42, "bad"] } },
855
+ { filters: {} },
856
+ ];
857
+
858
+ for (const payload of payloads) {
859
+ const result = await listDef!.handler(mockCtx, payload);
860
+ expect(result).toHaveProperty("data");
861
+ expect(result).toHaveProperty("total");
862
+ }
863
+ });
864
+ });
865
+
866
+ // ═══════════════════════════════════════════════════════════════════════════════
867
+ // 14. v3 보안 강화 — 재귀 깊이 제한 + like/ilike 타입 검증
868
+ // ═══════════════════════════════════════════════════════════════════════════════
869
+
870
+ describe("v3 보안 강화 — 재귀 깊이 제한", () => {
871
+ it("MAX_FILTER_DEPTH(5) 초과 시 조건 묵살", () => {
872
+ const table = pgTable("depth_test", {
873
+ id: serial("id").primaryKey(),
874
+ name: text("name"),
875
+ });
876
+
877
+ // 깊이 6단계 중첩 — MAX_FILTER_DEPTH=5 초과
878
+ const deepFilter = {
879
+ OR: [{ AND: [{ OR: [{ AND: [{ OR: [{ AND: [{ name: "deep" }] }] }] }] }] }],
880
+ };
881
+
882
+ const result = parseFilterNode(deepFilter, table);
883
+ // 깊이 초과된 부분이 묵살되어 결과가 undefined이거나 부분 조건만 남음
884
+ // 중요한 것은 스택 오버플로 없이 정상 반환
885
+ expect(() => parseFilterNode(deepFilter, table)).not.toThrow();
886
+ });
887
+
888
+ it("MAX_FILTER_DEPTH 이내의 중첩은 정상 동작", () => {
889
+ const table = pgTable("depth_ok_test", {
890
+ id: serial("id").primaryKey(),
891
+ name: text("name"),
892
+ status: text("status"),
893
+ });
894
+
895
+ // 깊이 3단계 — 정상 범위
896
+ const normalFilter = {
897
+ OR: [
898
+ { AND: [{ OR: [{ name: "test" }] }] },
899
+ { status: "active" },
900
+ ],
901
+ };
902
+
903
+ const result = parseFilterNode(normalFilter, table);
904
+ expect(result).toBeDefined();
905
+ });
906
+
907
+ it("극단적 깊이(100단계)에서도 크래시 없음", () => {
908
+ const table = pgTable("depth_extreme_test", {
909
+ id: serial("id").primaryKey(),
910
+ name: text("name"),
911
+ });
912
+
913
+ // 100단계 중첩 생성
914
+ let filter: any = { name: "leaf" };
915
+ for (let i = 0; i < 100; i++) {
916
+ filter = i % 2 === 0 ? { OR: [filter] } : { AND: [filter] };
917
+ }
918
+
919
+ expect(() => parseFilterNode(filter, table)).not.toThrow();
920
+ // 깊이 초과로 조건 묵살 → undefined
921
+ const result = parseFilterNode(filter, table);
922
+ expect(result).toBeUndefined();
923
+ });
924
+ });
925
+
926
+ describe("v3 보안 강화 — like/ilike 타입 검증", () => {
927
+ const table = pgTable("like_type_test", {
928
+ id: serial("id").primaryKey(),
929
+ name: text("name"),
930
+ });
931
+
932
+ it("like에 문자열이 아닌 값 전달 시 undefined 반환", () => {
933
+ expect(applyFilterOp(table.name, "like", 12345)).toBeUndefined();
934
+ expect(applyFilterOp(table.name, "like", null)).toBeUndefined();
935
+ expect(applyFilterOp(table.name, "like", { evil: true })).toBeUndefined();
936
+ });
937
+
938
+ it("ilike에 문자열이 아닌 값 전달 시 undefined 반환", () => {
939
+ expect(applyFilterOp(table.name, "ilike", 99)).toBeUndefined();
940
+ expect(applyFilterOp(table.name, "ilike", ["array"])).toBeUndefined();
941
+ });
942
+
943
+ it("like/ilike에 정상 문자열 전달 시 SQL 반환", () => {
944
+ expect(applyFilterOp(table.name, "like", "%test%")).toBeDefined();
945
+ expect(applyFilterOp(table.name, "ilike", "%TEST%")).toBeDefined();
946
+ });
947
+ });
@@ -0,0 +1,170 @@
1
+ /**
2
+ * packages/core/src/__tests__/dist-exports.test.ts
3
+ *
4
+ * dist/ 빌드 산출물의 필수 export 검증 — npm publish 전 안전망.
5
+ *
6
+ * 이 테스트의 존재 이유:
7
+ * 2026-04-02 사고: 소스코드에 crud v2가 완전 구현되어 있었으나,
8
+ * dist/index.js에 `crud` export가 누락된 구버전이 npm에 배포됨.
9
+ * crud.test.ts(16 pass)가 src/crud.ts를 직접 import하므로
10
+ * dist/ 빌드 결과물의 정합성은 검증하지 못했음.
11
+ *
12
+ * 검증 항목:
13
+ * 1. dist/index.js 파일 존재
14
+ * 2. 필수 named export 존재 (crud, query, mutation, v, ...)
15
+ * 3. gencowCrud deprecated alias 존재 (하위호환)
16
+ * 4. crud가 실제 함수인지 (typeof === "function")
17
+ * 5. dist/index.js의 crud와 src/crud.ts의 crud가 동일 함수인지
18
+ *
19
+ * Run: bun test packages/core/src/__tests__/dist-exports.test.ts
20
+ *
21
+ * @see docs/analysis/analysis-test032-gencow-crud-api-mismatch.md
22
+ */
23
+
24
+ import { describe, it, expect } from "bun:test";
25
+ import { existsSync } from "fs";
26
+ import { resolve, dirname } from "path";
27
+ import { fileURLToPath } from "url";
28
+
29
+ const __dirname = dirname(fileURLToPath(import.meta.url));
30
+ const CORE_ROOT = resolve(__dirname, "../..");
31
+ const DIST_INDEX = resolve(CORE_ROOT, "dist/index.js");
32
+
33
+ // ═══════════════════════════════════════════════════════════════════════════════
34
+ // 1. dist/ 파일 존재 확인
35
+ // ═══════════════════════════════════════════════════════════════════════════════
36
+
37
+ describe("dist/ 빌드 산출물 존재", () => {
38
+ it("dist/index.js 파일이 존재한다", () => {
39
+ expect(existsSync(DIST_INDEX)).toBe(true);
40
+ });
41
+
42
+ it("dist/crud.js 파일이 존재한다", () => {
43
+ expect(existsSync(resolve(CORE_ROOT, "dist/crud.js"))).toBe(true);
44
+ });
45
+
46
+ it("dist/reactive.js 파일이 존재한다", () => {
47
+ expect(existsSync(resolve(CORE_ROOT, "dist/reactive.js"))).toBe(true);
48
+ });
49
+
50
+ it("dist/v.js 파일이 존재한다", () => {
51
+ expect(existsSync(resolve(CORE_ROOT, "dist/v.js"))).toBe(true);
52
+ });
53
+ });
54
+
55
+ // ═══════════════════════════════════════════════════════════════════════════════
56
+ // 2. 필수 named export 존재 확인
57
+ // ═══════════════════════════════════════════════════════════════════════════════
58
+
59
+ describe("dist/index.js 필수 export", () => {
60
+ let distModule: Record<string, unknown>;
61
+
62
+ // dist/index.js를 dynamic import (빌드 결과물 검증)
63
+ it("dist/index.js를 import할 수 있다", async () => {
64
+ distModule = await import(DIST_INDEX);
65
+ expect(distModule).toBeDefined();
66
+ });
67
+
68
+ // ── Core API exports ──────────────────────────────────────
69
+
70
+ it("crud export가 존재하고 함수이다", async () => {
71
+ if (!distModule) distModule = await import(DIST_INDEX);
72
+ expect(distModule.crud).toBeDefined();
73
+ expect(typeof distModule.crud).toBe("function");
74
+ });
75
+
76
+ it("gencowCrud deprecated alias가 존재하고 crud와 동일하다", async () => {
77
+ if (!distModule) distModule = await import(DIST_INDEX);
78
+ expect(distModule.gencowCrud).toBeDefined();
79
+ expect(distModule.gencowCrud).toBe(distModule.crud);
80
+ });
81
+
82
+ it("query export가 존재하고 함수이다", async () => {
83
+ if (!distModule) distModule = await import(DIST_INDEX);
84
+ expect(distModule.query).toBeDefined();
85
+ expect(typeof distModule.query).toBe("function");
86
+ });
87
+
88
+ it("mutation export가 존재하고 함수이다", async () => {
89
+ if (!distModule) distModule = await import(DIST_INDEX);
90
+ expect(distModule.mutation).toBeDefined();
91
+ expect(typeof distModule.mutation).toBe("function");
92
+ });
93
+
94
+ it("v export가 존재하고 객체이다", async () => {
95
+ if (!distModule) distModule = await import(DIST_INDEX);
96
+ expect(distModule.v).toBeDefined();
97
+ expect(typeof distModule.v).toBe("object");
98
+ });
99
+
100
+ it("httpAction export가 존재하고 함수이다", async () => {
101
+ if (!distModule) distModule = await import(DIST_INDEX);
102
+ expect(distModule.httpAction).toBeDefined();
103
+ expect(typeof distModule.httpAction).toBe("function");
104
+ });
105
+
106
+ // ── Registry exports (codegen 의존) ────────────────────────
107
+
108
+ it("getRegisteredQueries export가 존재한다", async () => {
109
+ if (!distModule) distModule = await import(DIST_INDEX);
110
+ expect(distModule.getRegisteredQueries).toBeDefined();
111
+ expect(typeof distModule.getRegisteredQueries).toBe("function");
112
+ });
113
+
114
+ it("getRegisteredMutations export가 존재한다", async () => {
115
+ if (!distModule) distModule = await import(DIST_INDEX);
116
+ expect(distModule.getRegisteredMutations).toBeDefined();
117
+ expect(typeof distModule.getRegisteredMutations).toBe("function");
118
+ });
119
+
120
+ // ── RLS + Auth exports ─────────────────────────────────────
121
+
122
+ it("ownerRls export가 존재한다", async () => {
123
+ if (!distModule) distModule = await import(DIST_INDEX);
124
+ expect(distModule.ownerRls).toBeDefined();
125
+ });
126
+
127
+ it("defineAuth export가 존재한다", async () => {
128
+ if (!distModule) distModule = await import(DIST_INDEX);
129
+ expect(distModule.defineAuth).toBeDefined();
130
+ });
131
+
132
+ it("cronJobs export가 존재한다", async () => {
133
+ if (!distModule) distModule = await import(DIST_INDEX);
134
+ expect(distModule.cronJobs).toBeDefined();
135
+ });
136
+ });
137
+
138
+ // ═══════════════════════════════════════════════════════════════════════════════
139
+ // 3. dist/crud.js가 v2 구현인지 확인 (커링 구조가 아닌지)
140
+ // ═══════════════════════════════════════════════════════════════════════════════
141
+
142
+ describe("dist/crud.js — v2 구현 검증", () => {
143
+ it("crud(table) 시그니처이다 (커링 gencowCrud(db)(table) 아님)", async () => {
144
+ const distModule = await import(DIST_INDEX);
145
+ const { crud } = distModule;
146
+
147
+ // v2: crud(table, options?) → { list, get, create, update, remove }
148
+ // 구버전: gencowCrud(db) → (table, options?) → { create, findById, list, ... }
149
+
150
+ // 실제 Drizzle 테이블로 테스트
151
+ const { pgTable, serial, text } = await import("drizzle-orm/pg-core");
152
+ const testTable = pgTable("dist_smoke_test", {
153
+ id: serial("id").primaryKey(),
154
+ name: text("name"),
155
+ });
156
+
157
+ const result = crud(testTable, { public: true });
158
+
159
+ // v2는 { list, get, create, update, remove } 반환
160
+ expect(result).toHaveProperty("list");
161
+ expect(result).toHaveProperty("get");
162
+ expect(result).toHaveProperty("create");
163
+ expect(result).toHaveProperty("update");
164
+ expect(result).toHaveProperty("remove");
165
+
166
+ // 구버전(커링)은 함수를 반환하므로 이 검증이 실패함
167
+ expect(typeof result).toBe("object");
168
+ expect(typeof result.list).not.toBe("undefined");
169
+ });
170
+ });