@gencow/core 0.1.24 → 0.1.25

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 (73) hide show
  1. package/dist/crud.d.ts +2 -2
  2. package/dist/crud.js +225 -208
  3. package/dist/index.d.ts +5 -5
  4. package/dist/index.js +2 -2
  5. package/dist/reactive.js +10 -3
  6. package/dist/retry.js +1 -1
  7. package/dist/rls-db.d.ts +2 -2
  8. package/dist/rls-db.js +1 -5
  9. package/dist/scheduler.d.ts +2 -0
  10. package/dist/scheduler.js +16 -6
  11. package/dist/server.d.ts +0 -1
  12. package/dist/server.js +0 -1
  13. package/dist/storage.js +29 -22
  14. package/dist/v.d.ts +2 -2
  15. package/dist/workflow.js +4 -11
  16. package/dist/workflows-api.js +5 -12
  17. package/package.json +46 -42
  18. package/src/__tests__/auth.test.ts +90 -86
  19. package/src/__tests__/crons.test.ts +69 -67
  20. package/src/__tests__/crud-codegen-integration.test.ts +164 -170
  21. package/src/__tests__/crud-owner-rls.test.ts +308 -301
  22. package/src/__tests__/crud.test.ts +694 -711
  23. package/src/__tests__/dist-exports.test.ts +120 -120
  24. package/src/__tests__/fixtures/basic/auth.ts +16 -16
  25. package/src/__tests__/fixtures/basic/drizzle.config.ts +1 -4
  26. package/src/__tests__/fixtures/basic/index.ts +1 -1
  27. package/src/__tests__/fixtures/basic/schema.ts +1 -1
  28. package/src/__tests__/fixtures/basic/tasks.ts +4 -4
  29. package/src/__tests__/fixtures/common/auth-schema.ts +38 -34
  30. package/src/__tests__/helpers/basic-rls-fixture.ts +80 -78
  31. package/src/__tests__/helpers/pglite-migrations.ts +2 -5
  32. package/src/__tests__/helpers/pglite-rls-session.ts +13 -16
  33. package/src/__tests__/helpers/seed-like-fill.ts +50 -44
  34. package/src/__tests__/helpers/test-gencow-ctx-rls.ts +4 -7
  35. package/src/__tests__/httpaction.test.ts +91 -91
  36. package/src/__tests__/image-optimization.test.ts +570 -574
  37. package/src/__tests__/load.test.ts +321 -308
  38. package/src/__tests__/network-sim.test.ts +238 -215
  39. package/src/__tests__/reactive.test.ts +380 -358
  40. package/src/__tests__/retry.test.ts +99 -84
  41. package/src/__tests__/rls-crud-basic.test.ts +172 -245
  42. package/src/__tests__/rls-crud-no-owner-rls-pglite.test.ts +81 -81
  43. package/src/__tests__/rls-custom-mutation-handlers.test.ts +47 -94
  44. package/src/__tests__/rls-custom-query-handlers.test.ts +92 -92
  45. package/src/__tests__/rls-db-leased-connection.test.ts +2 -6
  46. package/src/__tests__/rls-session-and-policies.test.ts +181 -199
  47. package/src/__tests__/scheduler-durable-v2.test.ts +199 -181
  48. package/src/__tests__/scheduler-durable.test.ts +117 -117
  49. package/src/__tests__/scheduler-exec.test.ts +258 -246
  50. package/src/__tests__/scheduler.test.ts +129 -111
  51. package/src/__tests__/storage.test.ts +282 -269
  52. package/src/__tests__/tsconfig.json +6 -6
  53. package/src/__tests__/validator.test.ts +236 -232
  54. package/src/__tests__/workflow.test.ts +309 -286
  55. package/src/__tests__/ws-integration.test.ts +223 -218
  56. package/src/__tests__/ws-scale.test.ts +168 -159
  57. package/src/auth-config.ts +18 -18
  58. package/src/auth.ts +106 -106
  59. package/src/crons.ts +77 -77
  60. package/src/crud.ts +523 -479
  61. package/src/index.ts +69 -5
  62. package/src/reactive.ts +357 -331
  63. package/src/retry.ts +51 -54
  64. package/src/rls-db.ts +195 -205
  65. package/src/rls.ts +33 -36
  66. package/src/scheduler.ts +237 -211
  67. package/src/server.ts +0 -1
  68. package/src/storage.ts +632 -593
  69. package/src/v.ts +119 -114
  70. package/src/workflow-types.ts +67 -70
  71. package/src/workflow.ts +99 -116
  72. package/src/workflows-api.ts +231 -241
  73. package/src/db.ts +0 -18
@@ -12,14 +12,11 @@ function listMigrationSqlFiles(migrationsDir: string): string[] {
12
12
  /**
13
13
  * Apply Drizzle-generated SQL from a folder of `.sql` files (split on `--> statement-breakpoint`).
14
14
  */
15
- export async function loadAndApplyMigrations(
16
- client: PGlite,
17
- migrationsDir: string
18
- ): Promise<void> {
15
+ export async function loadAndApplyMigrations(client: PGlite, migrationsDir: string): Promise<void> {
19
16
  const files = listMigrationSqlFiles(migrationsDir);
20
17
  if (files.length === 0) {
21
18
  throw new Error(
22
- `No .sql migration files in ${migrationsDir} (generate migrations with your Drizzle workflow)`
19
+ `No .sql migration files in ${migrationsDir} (generate migrations with your Drizzle workflow)`,
23
20
  );
24
21
  }
25
22
  for (const filePath of files) {
@@ -4,10 +4,10 @@ import type { PGlite } from "@electric-sql/pglite";
4
4
  export const DEFAULT_PGLITE_RLS_APP_ROLE = "gencow_rls_app";
5
5
 
6
6
  function quoteIdent(name: string): string {
7
- if (!/^[a-z_][a-z0-9_]*$/i.test(name)) {
8
- throw new Error(`Invalid SQL identifier: ${name}`);
9
- }
10
- return `"${name.replace(/"/g, '""')}"`;
7
+ if (!/^[a-z_][a-z0-9_]*$/i.test(name)) {
8
+ throw new Error(`Invalid SQL identifier: ${name}`);
9
+ }
10
+ return `"${name.replace(/"/g, '""')}"`;
11
11
  }
12
12
 
13
13
  /**
@@ -21,34 +21,31 @@ function quoteIdent(name: string): string {
21
21
  * Call {@link setPgliteSessionRole} on the same `PGlite` instance before running app queries.
22
22
  */
23
23
  export async function createPgliteRlsAppRole(
24
- client: PGlite,
25
- options?: { roleName?: string }
24
+ client: PGlite,
25
+ options?: { roleName?: string },
26
26
  ): Promise<string> {
27
- const roleName = options?.roleName ?? DEFAULT_PGLITE_RLS_APP_ROLE;
28
- const role = quoteIdent(roleName);
27
+ const roleName = options?.roleName ?? DEFAULT_PGLITE_RLS_APP_ROLE;
28
+ const role = quoteIdent(roleName);
29
29
 
30
- await client.exec(`
30
+ await client.exec(`
31
31
  CREATE ROLE ${role} LOGIN;
32
32
  GRANT USAGE ON SCHEMA public TO ${role};
33
33
  GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO ${role};
34
34
  GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO ${role};
35
35
  `);
36
36
 
37
- return roleName;
37
+ return roleName;
38
38
  }
39
39
 
40
40
  /**
41
41
  * Run follow-up queries in this session as the given role (must not be table owner so RLS applies).
42
42
  * The bootstrap user must be allowed to `SET ROLE` (e.g. superuser, or `GRANT rls_role TO bootstrap`).
43
43
  */
44
- export async function setPgliteSessionRole(
45
- client: PGlite,
46
- roleName: string
47
- ): Promise<void> {
48
- await client.exec(`SET ROLE ${quoteIdent(roleName)}`);
44
+ export async function setPgliteSessionRole(client: PGlite, roleName: string): Promise<void> {
45
+ await client.exec(`SET ROLE ${quoteIdent(roleName)}`);
49
46
  }
50
47
 
51
48
  /** Restore session user to the original login role (typically the PGlite bootstrap user). */
52
49
  export async function resetPgliteSessionRole(client: PGlite): Promise<void> {
53
- await client.exec("RESET ROLE");
50
+ await client.exec("RESET ROLE");
54
51
  }
@@ -1,17 +1,18 @@
1
1
  /**
2
2
  * Merge explicit partial insert rows with drizzle-seed-style values for missing columns.
3
- * Uses drizzle-seed's `SeedService.selectGeneratorForPostgresColumn` so new schema columns get
3
+ * Uses drizzle-seed's `SeedService.generatePossibleGenerators` so new schema columns get
4
4
  * the same generators as `seed()` without listing every column in test fixtures.
5
5
  */
6
6
 
7
- import { getTableColumns, getTableName } from "drizzle-orm";
7
+ import { getColumns, getTableName } from "drizzle-orm";
8
8
  import type { InferInsertModel } from "drizzle-orm";
9
9
  import type { PgColumn, PgTable } from "drizzle-orm/pg-core";
10
+ import { getTableConfig } from "drizzle-orm/pg-core";
10
11
  import { SeedService } from "drizzle-seed";
11
12
 
12
13
  type SeedSvc = InstanceType<typeof SeedService>;
13
- type SeedTable = Parameters<SeedSvc["selectGeneratorForPostgresColumn"]>[0];
14
- type SeedColumn = Parameters<SeedSvc["selectGeneratorForPostgresColumn"]>[1];
14
+ type SeedTable = Parameters<SeedSvc["generatePossibleGenerators"]>[1][number];
15
+ type SeedColumn = SeedTable["columns"][number];
15
16
 
16
17
  /** Fields drizzle-seed reads from ORM columns (mirrors `getPostgresInfo` in drizzle-seed). */
17
18
  type PgColumnSeedExtras = PgColumn & {
@@ -81,40 +82,40 @@ function getAllBaseColumns(baseColumn: PgColumn): NonNullable<SeedColumn["baseCo
81
82
  isUnique: baseColumn.isUnique,
82
83
  notNull: baseColumn.notNull,
83
84
  primary: baseColumn.primary,
84
- baseColumn:
85
- b.baseColumn === undefined ? undefined : getAllBaseColumns(b.baseColumn),
85
+ baseColumn: b.baseColumn === undefined ? undefined : getAllBaseColumns(b.baseColumn),
86
86
  };
87
87
  }
88
88
 
89
89
  /**
90
90
  * Drizzle `PgTable` → drizzle-seed `Table` (same shape as `getPostgresInfo` in drizzle-seed).
91
91
  */
92
- export function drizzlePgTableToSeedTable(
93
- pgTable: PgTable,
94
- schemaKey: string
95
- ): SeedTable {
96
- const colsMap = getTableColumns(pgTable) as Record<string, PgColumn>;
92
+ export function drizzlePgTableToSeedTable(pgTable: PgTable, schemaKey: string): SeedTable {
93
+ const colsMap = getColumns(pgTable) as Record<string, PgColumn>;
94
+ const tableConfig = getTableConfig(pgTable);
97
95
  const columns: SeedColumn[] = Object.entries(colsMap).map(([tsName, column]) => {
98
96
  const c = pgColumnForSeed(column);
99
97
  return {
100
- name: tsName,
101
- columnType: column.getSQLType(),
102
- typeParams: getTypeParams(column.getSQLType()),
103
- dataType: column.dataType,
104
- size: c.size,
105
- hasDefault: column.hasDefault,
106
- default: column.default,
107
- enumValues: column.enumValues,
108
- isUnique: column.isUnique,
109
- notNull: column.notNull,
110
- primary: column.primary,
111
- generatedIdentityType: column.generatedIdentity?.type,
112
- baseColumn:
113
- c.baseColumn === undefined ? undefined : getAllBaseColumns(c.baseColumn),
114
- };
98
+ name: tsName,
99
+ columnType: column.getSQLType(),
100
+ typeParams: getTypeParams(column.getSQLType()),
101
+ dataType: column.dataType,
102
+ size: c.size,
103
+ hasDefault: column.hasDefault,
104
+ default: column.default,
105
+ enumValues: column.enumValues,
106
+ isUnique: column.isUnique,
107
+ notNull: column.notNull,
108
+ primary: column.primary,
109
+ generatedIdentityType: column.generatedIdentity?.type,
110
+ identity: column.generatedIdentity !== undefined,
111
+ baseColumn: c.baseColumn === undefined ? undefined : getAllBaseColumns(c.baseColumn),
112
+ };
115
113
  });
116
114
  const primaryKeys = Object.keys(colsMap).filter((k) => colsMap[k]!.primary);
117
- return { name: schemaKey, columns, primaryKeys };
115
+ const uniqueConstraints = tableConfig.uniqueConstraints.map((uc) =>
116
+ uc.columns.map((c) => (c as PgColumn).name),
117
+ );
118
+ return { name: schemaKey, columns, primaryKeys, uniqueConstraints };
118
119
  }
119
120
 
120
121
  /**
@@ -127,22 +128,32 @@ export function drizzlePgTableToSeedTable(
127
128
  export function fillPartialRowsForInsert<T extends PgTable>(
128
129
  pgTable: T,
129
130
  partialRows: Array<Partial<InferInsertModel<T>>>,
130
- options?: { seed?: number; version?: number; schemaKey?: string }
131
+ options?: { seed?: number; version?: number; schemaKey?: string },
131
132
  ): InferInsertModel<T>[] {
132
133
  const schemaKey = options?.schemaKey ?? getTableName(pgTable);
133
134
  const seedService = new SeedService();
134
- /** `selectVersionOfGenerator` reads this; `seed()` sets it via `generatePossibleGenerators`. */
135
- Reflect.set(seedService, "version", options?.version ?? 2);
136
135
  const seedTable = drizzlePgTableToSeedTable(pgTable, schemaKey);
137
- const colsMap = getTableColumns(pgTable) as Record<string, PgColumn>;
136
+ const colsMap = getColumns(pgTable) as Record<string, PgColumn>;
138
137
  const tsNames = Object.keys(colsMap);
139
138
  const baseSeed = options?.seed ?? 0;
140
139
  const count = partialRows.length;
141
140
 
142
- const generators: Record<
143
- string,
144
- { generate: (args: { i: number }) => unknown }
145
- > = {};
141
+ // Build generator map for all columns at once using the v1 API
142
+ const tablePossibleGens = seedService.generatePossibleGenerators(
143
+ "postgresql",
144
+ [seedTable],
145
+ [],
146
+ undefined,
147
+ { version: options?.version ?? 2 },
148
+ );
149
+ const colGenMap: Record<string, ReturnType<SeedSvc["selectVersionOfGenerator"]>> = {};
150
+ for (const colGen of tablePossibleGens[0]!.columnsPossibleGenerators) {
151
+ if (colGen.generator !== undefined) {
152
+ colGenMap[colGen.columnName] = seedService.selectVersionOfGenerator(colGen.generator);
153
+ }
154
+ }
155
+
156
+ const generators: Record<string, { generate: (args: { i: number }) => unknown }> = {};
146
157
 
147
158
  for (const tsName of tsNames) {
148
159
  const drizzleCol = colsMap[tsName]!;
@@ -153,17 +164,12 @@ export function fillPartialRowsForInsert<T extends PgTable>(
153
164
  if (!needsGen) {
154
165
  continue;
155
166
  }
156
- const seedCol = seedTable.columns.find((c) => c.name === tsName);
157
- if (!seedCol) {
158
- throw new Error(`[seed-like-fill] column ${tsName} not in seed table`);
159
- }
160
- const rawGen = seedService.selectGeneratorForPostgresColumn(seedTable, seedCol);
161
- if (rawGen === undefined) {
167
+ const gen = colGenMap[tsName];
168
+ if (gen === undefined) {
162
169
  throw new Error(
163
- `[seed-like-fill] unsupported column type for ${schemaKey}.${tsName}: ${seedCol.columnType}`
170
+ `[seed-like-fill] unsupported column type for ${schemaKey}.${tsName}: ${drizzleCol.getSQLType()}`,
164
171
  );
165
172
  }
166
- const gen = seedService.selectVersionOfGenerator(rawGen);
167
173
  const pRNGSeed = baseSeed + hashSeed(`${schemaKey}.${tsName}`);
168
174
  gen.init({ count, seed: pRNGSeed });
169
175
  generators[tsName] = gen;
@@ -184,7 +190,7 @@ export function fillPartialRowsForInsert<T extends PgTable>(
184
190
  const g = generators[tsName];
185
191
  if (!g) {
186
192
  throw new Error(
187
- `[seed-like-fill] missing generator for ${schemaKey}.${tsName} (partial row without value)`
193
+ `[seed-like-fill] missing generator for ${schemaKey}.${tsName} (partial row without value)`,
188
194
  );
189
195
  }
190
196
  row[tsName] = g.generate({ i });
@@ -1,12 +1,9 @@
1
1
  import { drizzle } from "drizzle-orm/pglite";
2
2
 
3
- import { createRlsDb } from "../../rls-db";
4
- import type { GencowCtx, UserIdentity } from "../../reactive";
3
+ import { createRlsDb } from "../../rls-db.js";
4
+ import type { GencowCtx, UserIdentity } from "../../reactive.js";
5
5
 
6
- export function makeTestGencowCtxWithRls(
7
- db: ReturnType<typeof drizzle>,
8
- identity: UserIdentity
9
- ): GencowCtx {
6
+ export function makeTestGencowCtxWithRls(db: ReturnType<typeof drizzle>, identity: UserIdentity): GencowCtx {
10
7
  const scoped = createRlsDb(db, { userId: identity.id });
11
8
  return {
12
9
  db: scoped,
@@ -33,7 +30,7 @@ const FORCE_ROLLBACK = Symbol("force-rollback");
33
30
  export async function runWithRollbackTestGencowCtxWithRls<T>(
34
31
  db: ReturnType<typeof drizzle>,
35
32
  identity: UserIdentity,
36
- run: (ctx: GencowCtx) => Promise<T>
33
+ run: (ctx: GencowCtx) => Promise<T>,
37
34
  ): Promise<T> {
38
35
  let result!: T;
39
36
 
@@ -7,116 +7,116 @@
7
7
  */
8
8
 
9
9
  import { describe, it, expect, beforeEach } from "bun:test";
10
- import { httpAction, getRegisteredHttpActions } from "../reactive";
10
+ import { httpAction, getRegisteredHttpActions } from "../reactive.js";
11
11
 
12
12
  // Clean up httpAction registry between tests
13
13
  // Note: httpAction uses globalThis.__gencow_httpActionRegistry (push-only array)
14
14
  // We clear it in beforeEach to isolate tests.
15
15
 
16
16
  describe("httpAction()", () => {
17
- const initialLength = getRegisteredHttpActions().length;
17
+ const initialLength = getRegisteredHttpActions().length;
18
18
 
19
- it("httpAction 등록 시 레지스트리에 추가된다", () => {
20
- const before = getRegisteredHttpActions().length;
19
+ it("httpAction 등록 시 레지스트리에 추가된다", () => {
20
+ const before = getRegisteredHttpActions().length;
21
21
 
22
- httpAction({
23
- method: "GET",
24
- path: "/test/health",
25
- handler: async () => ({ body: { status: "ok" } }),
26
- });
27
-
28
- const after = getRegisteredHttpActions().length;
29
- expect(after).toBe(before + 1);
30
- });
31
-
32
- it("method, path가 정확히 설정된다", () => {
33
- httpAction({
34
- method: "POST",
35
- path: "/webhook/stripe",
36
- handler: async () => ({ body: { received: true } }),
37
- });
38
-
39
- const actions = getRegisteredHttpActions();
40
- const last = actions[actions.length - 1];
41
- expect(last.method).toBe("POST");
42
- expect(last.path).toBe("/webhook/stripe");
22
+ httpAction({
23
+ method: "GET",
24
+ path: "/test/health",
25
+ handler: async () => ({ body: { status: "ok" } }),
43
26
  });
44
27
 
45
- it("public 기본값은 false이다", () => {
46
- httpAction({
47
- method: "GET",
48
- path: "/test/private",
49
- handler: async () => ({ body: {} }),
50
- });
28
+ const after = getRegisteredHttpActions().length;
29
+ expect(after).toBe(before + 1);
30
+ });
51
31
 
52
- const actions = getRegisteredHttpActions();
53
- const last = actions[actions.length - 1];
54
- expect(last.isPublic).toBe(false);
32
+ it("method, path가 정확히 설정된다", () => {
33
+ httpAction({
34
+ method: "POST",
35
+ path: "/webhook/stripe",
36
+ handler: async () => ({ body: { received: true } }),
55
37
  });
56
38
 
57
- it("public: true 설정 시 isPublic === true", () => {
58
- httpAction({
59
- method: "GET",
60
- path: "/test/public-endpoint",
61
- public: true,
62
- handler: async () => ({ body: {} }),
63
- });
64
-
65
- const actions = getRegisteredHttpActions();
66
- const last = actions[actions.length - 1];
67
- expect(last.isPublic).toBe(true);
39
+ const actions = getRegisteredHttpActions();
40
+ const last = actions[actions.length - 1];
41
+ expect(last.method).toBe("POST");
42
+ expect(last.path).toBe("/webhook/stripe");
43
+ });
44
+
45
+ it("public 기본값은 false이다", () => {
46
+ httpAction({
47
+ method: "GET",
48
+ path: "/test/private",
49
+ handler: async () => ({ body: {} }),
68
50
  });
69
51
 
70
- it("handler가 HttpActionDef에 포함된다", () => {
71
- const handler = async () => ({ body: { ok: true } });
72
- httpAction({
73
- method: "PUT",
74
- path: "/test/with-handler",
75
- handler,
76
- });
77
-
78
- const actions = getRegisteredHttpActions();
79
- const last = actions[actions.length - 1];
80
- expect(last.handler).toBe(handler);
52
+ const actions = getRegisteredHttpActions();
53
+ const last = actions[actions.length - 1];
54
+ expect(last.isPublic).toBe(false);
55
+ });
56
+
57
+ it("public: true 설정 시 isPublic === true", () => {
58
+ httpAction({
59
+ method: "GET",
60
+ path: "/test/public-endpoint",
61
+ public: true,
62
+ handler: async () => ({ body: {} }),
81
63
  });
82
64
 
83
- it("모든 HTTP 메서드 지원 (GET, POST, PUT, DELETE, PATCH)", () => {
84
- const methods = ["GET", "POST", "PUT", "DELETE", "PATCH"] as const;
85
- const before = getRegisteredHttpActions().length;
86
-
87
- for (const method of methods) {
88
- httpAction({
89
- method,
90
- path: `/test/method-${method.toLowerCase()}`,
91
- handler: async () => ({ body: {} }),
92
- });
93
- }
94
-
95
- const after = getRegisteredHttpActions().length;
96
- expect(after).toBe(before + 5);
97
-
98
- const actions = getRegisteredHttpActions();
99
- const registered = actions.slice(-5).map(a => a.method);
100
- expect(registered).toEqual(["GET", "POST", "PUT", "DELETE", "PATCH"]);
65
+ const actions = getRegisteredHttpActions();
66
+ const last = actions[actions.length - 1];
67
+ expect(last.isPublic).toBe(true);
68
+ });
69
+
70
+ it("handler가 HttpActionDef에 포함된다", () => {
71
+ const handler = async () => ({ body: { ok: true } });
72
+ httpAction({
73
+ method: "PUT",
74
+ path: "/test/with-handler",
75
+ handler,
101
76
  });
102
77
 
103
- it("getRegisteredHttpActions()는 복사본을 반환한다 (원본 보호)", () => {
104
- const a = getRegisteredHttpActions();
105
- const b = getRegisteredHttpActions();
106
- expect(a).not.toBe(b);
107
- expect(a).toEqual(b);
78
+ const actions = getRegisteredHttpActions();
79
+ const last = actions[actions.length - 1];
80
+ expect(last.handler).toBe(handler);
81
+ });
82
+
83
+ it("모든 HTTP 메서드 지원 (GET, POST, PUT, DELETE, PATCH)", () => {
84
+ const methods = ["GET", "POST", "PUT", "DELETE", "PATCH"] as const;
85
+ const before = getRegisteredHttpActions().length;
86
+
87
+ for (const method of methods) {
88
+ httpAction({
89
+ method,
90
+ path: `/test/method-${method.toLowerCase()}`,
91
+ handler: async () => ({ body: {} }),
92
+ });
93
+ }
94
+
95
+ const after = getRegisteredHttpActions().length;
96
+ expect(after).toBe(before + 5);
97
+
98
+ const actions = getRegisteredHttpActions();
99
+ const registered = actions.slice(-5).map((a) => a.method);
100
+ expect(registered).toEqual(["GET", "POST", "PUT", "DELETE", "PATCH"]);
101
+ });
102
+
103
+ it("getRegisteredHttpActions()는 복사본을 반환한다 (원본 보호)", () => {
104
+ const a = getRegisteredHttpActions();
105
+ const b = getRegisteredHttpActions();
106
+ expect(a).not.toBe(b);
107
+ expect(a).toEqual(b);
108
+ });
109
+
110
+ it("Hono 경로 패턴 (/api/:id) 지원", () => {
111
+ httpAction({
112
+ method: "GET",
113
+ path: "/api/apps/:id/status",
114
+ public: true,
115
+ handler: async () => ({ body: {} }),
108
116
  });
109
117
 
110
- it("Hono 경로 패턴 (/api/:id) 지원", () => {
111
- httpAction({
112
- method: "GET",
113
- path: "/api/apps/:id/status",
114
- public: true,
115
- handler: async () => ({ body: {} }),
116
- });
117
-
118
- const actions = getRegisteredHttpActions();
119
- const last = actions[actions.length - 1];
120
- expect(last.path).toBe("/api/apps/:id/status");
121
- });
118
+ const actions = getRegisteredHttpActions();
119
+ const last = actions[actions.length - 1];
120
+ expect(last.path).toBe("/api/apps/:id/status");
121
+ });
122
122
  });