@gencow/core 0.1.24 → 0.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/crud.d.ts +2 -2
- package/dist/crud.js +225 -208
- package/dist/index.d.ts +5 -5
- package/dist/index.js +2 -2
- package/dist/reactive.js +10 -3
- package/dist/retry.js +1 -1
- package/dist/rls-db.d.ts +2 -2
- package/dist/rls-db.js +1 -5
- package/dist/scheduler.d.ts +2 -0
- package/dist/scheduler.js +16 -6
- package/dist/server.d.ts +0 -1
- package/dist/server.js +0 -1
- package/dist/storage.js +29 -22
- package/dist/v.d.ts +2 -2
- package/dist/workflow.js +4 -11
- package/dist/workflows-api.js +5 -12
- package/package.json +45 -42
- package/src/__tests__/auth.test.ts +90 -86
- package/src/__tests__/crons.test.ts +69 -67
- package/src/__tests__/crud-codegen-integration.test.ts +164 -170
- package/src/__tests__/crud-owner-rls.test.ts +308 -301
- package/src/__tests__/crud.test.ts +694 -711
- package/src/__tests__/dist-exports.test.ts +120 -120
- package/src/__tests__/fixtures/basic/auth.ts +16 -16
- package/src/__tests__/fixtures/basic/drizzle.config.ts +1 -4
- package/src/__tests__/fixtures/basic/index.ts +1 -1
- package/src/__tests__/fixtures/basic/schema.ts +1 -1
- package/src/__tests__/fixtures/basic/tasks.ts +4 -4
- package/src/__tests__/fixtures/common/auth-schema.ts +38 -34
- package/src/__tests__/helpers/basic-rls-fixture.ts +80 -78
- package/src/__tests__/helpers/pglite-migrations.ts +2 -5
- package/src/__tests__/helpers/pglite-rls-session.ts +13 -16
- package/src/__tests__/helpers/seed-like-fill.ts +47 -41
- package/src/__tests__/helpers/test-gencow-ctx-rls.ts +4 -7
- package/src/__tests__/httpaction.test.ts +91 -91
- package/src/__tests__/image-optimization.test.ts +570 -574
- package/src/__tests__/load.test.ts +321 -308
- package/src/__tests__/network-sim.test.ts +238 -215
- package/src/__tests__/reactive.test.ts +380 -358
- package/src/__tests__/retry.test.ts +99 -84
- package/src/__tests__/rls-crud-basic.test.ts +172 -245
- package/src/__tests__/rls-crud-no-owner-rls-pglite.test.ts +81 -81
- package/src/__tests__/rls-custom-mutation-handlers.test.ts +47 -94
- package/src/__tests__/rls-custom-query-handlers.test.ts +92 -92
- package/src/__tests__/rls-db-leased-connection.test.ts +2 -6
- package/src/__tests__/rls-session-and-policies.test.ts +181 -199
- package/src/__tests__/scheduler-durable-v2.test.ts +199 -181
- package/src/__tests__/scheduler-durable.test.ts +117 -117
- package/src/__tests__/scheduler-exec.test.ts +258 -246
- package/src/__tests__/scheduler.test.ts +129 -111
- package/src/__tests__/storage.test.ts +282 -269
- package/src/__tests__/tsconfig.json +6 -6
- package/src/__tests__/validator.test.ts +236 -232
- package/src/__tests__/workflow.test.ts +309 -286
- package/src/__tests__/ws-integration.test.ts +223 -218
- package/src/__tests__/ws-scale.test.ts +168 -159
- package/src/auth-config.ts +18 -18
- package/src/auth.ts +106 -106
- package/src/crons.ts +77 -77
- package/src/crud.ts +523 -479
- package/src/index.ts +69 -5
- package/src/reactive.ts +357 -331
- package/src/retry.ts +51 -54
- package/src/rls-db.ts +195 -205
- package/src/rls.ts +33 -36
- package/src/scheduler.ts +237 -211
- package/src/server.ts +0 -1
- package/src/storage.ts +632 -593
- package/src/v.ts +119 -114
- package/src/workflow-types.ts +67 -70
- package/src/workflow.ts +99 -116
- package/src/workflows-api.ts +231 -241
- package/dist/db.d.ts +0 -13
- package/dist/db.js +0 -16
- 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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
25
|
-
|
|
24
|
+
client: PGlite,
|
|
25
|
+
options?: { roleName?: string },
|
|
26
26
|
): Promise<string> {
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
const roleName = options?.roleName ?? DEFAULT_PGLITE_RLS_APP_ROLE;
|
|
28
|
+
const role = quoteIdent(roleName);
|
|
29
29
|
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
7
|
import { getTableColumns, 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["
|
|
14
|
-
type SeedColumn =
|
|
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 {
|
|
92
|
+
export function drizzlePgTableToSeedTable(pgTable: PgTable, schemaKey: string): SeedTable {
|
|
96
93
|
const colsMap = getTableColumns(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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
136
|
const colsMap = getTableColumns(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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
157
|
-
if (
|
|
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}: ${
|
|
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
|
-
|
|
17
|
+
const initialLength = getRegisteredHttpActions().length;
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
it("httpAction 등록 시 레지스트리에 추가된다", () => {
|
|
20
|
+
const before = getRegisteredHttpActions().length;
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
path: "/test/private",
|
|
49
|
-
handler: async () => ({ body: {} }),
|
|
50
|
-
});
|
|
28
|
+
const after = getRegisteredHttpActions().length;
|
|
29
|
+
expect(after).toBe(before + 1);
|
|
30
|
+
});
|
|
51
31
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
32
|
+
it("method, path가 정확히 설정된다", () => {
|
|
33
|
+
httpAction({
|
|
34
|
+
method: "POST",
|
|
35
|
+
path: "/webhook/stripe",
|
|
36
|
+
handler: async () => ({ body: { received: true } }),
|
|
55
37
|
});
|
|
56
38
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
});
|