@gencow/core 0.1.27 → 0.1.29

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 (130) hide show
  1. package/dist/auth-config.d.ts +92 -5
  2. package/dist/config.d.ts +107 -0
  3. package/dist/config.js +12 -0
  4. package/dist/context.d.ts +139 -0
  5. package/dist/context.js +3 -0
  6. package/dist/crud.d.ts +5 -5
  7. package/dist/crud.js +19 -35
  8. package/dist/document-types.d.ts +65 -0
  9. package/dist/document-types.js +15 -0
  10. package/dist/grounded-answer-types.d.ts +62 -0
  11. package/dist/grounded-answer-types.js +6 -0
  12. package/dist/http-action.d.ts +77 -0
  13. package/dist/http-action.js +41 -0
  14. package/dist/index.d.ts +30 -5
  15. package/dist/index.js +15 -2
  16. package/dist/platform-capacity-profile.d.ts +19 -0
  17. package/dist/platform-capacity-profile.js +94 -0
  18. package/dist/procedure.d.ts +58 -0
  19. package/dist/procedure.js +115 -0
  20. package/dist/rag-ingest-types.d.ts +39 -0
  21. package/dist/rag-ingest-types.js +1 -0
  22. package/dist/rag-operations-types.d.ts +81 -0
  23. package/dist/rag-operations-types.js +1 -0
  24. package/dist/rag-schema.d.ts +1466 -0
  25. package/dist/rag-schema.js +87 -0
  26. package/dist/reactive-mutation-types.d.ts +11 -0
  27. package/dist/reactive-mutation-types.js +1 -0
  28. package/dist/reactive-mutation.d.ts +51 -0
  29. package/dist/reactive-mutation.js +75 -0
  30. package/dist/reactive-query-types.d.ts +12 -0
  31. package/dist/reactive-query-types.js +1 -0
  32. package/dist/reactive-query.d.ts +14 -0
  33. package/dist/reactive-query.js +28 -0
  34. package/dist/reactive-realtime.d.ts +48 -0
  35. package/dist/reactive-realtime.js +236 -0
  36. package/dist/reactive.d.ts +29 -5
  37. package/dist/reactive.js +65 -0
  38. package/dist/rls-db.d.ts +9 -2
  39. package/dist/runtime-env-policy.d.ts +5 -0
  40. package/dist/runtime-env-policy.js +56 -0
  41. package/dist/search-types.d.ts +83 -0
  42. package/dist/search-types.js +1 -0
  43. package/dist/server.d.ts +1 -2
  44. package/dist/server.js +0 -1
  45. package/dist/storage-metering.d.ts +13 -0
  46. package/dist/storage-metering.js +18 -0
  47. package/dist/storage-shared.d.ts +36 -0
  48. package/dist/storage-shared.js +39 -0
  49. package/dist/storage.d.ts +5 -27
  50. package/dist/storage.js +30 -22
  51. package/dist/wake-app-result.d.ts +22 -0
  52. package/dist/wake-app-result.js +11 -0
  53. package/dist/workflow-types.d.ts +16 -2
  54. package/dist/workflow.d.ts +1 -1
  55. package/dist/workflow.js +136 -11
  56. package/dist/workflows-api.js +71 -3
  57. package/package.json +11 -7
  58. package/src/auth-config.ts +104 -3
  59. package/src/config.ts +119 -0
  60. package/src/context.ts +152 -0
  61. package/src/crud.ts +18 -35
  62. package/src/document-types.ts +102 -0
  63. package/src/grounded-answer-types.ts +78 -0
  64. package/src/http-action.ts +101 -0
  65. package/src/index.ts +142 -19
  66. package/src/platform-capacity-profile.ts +114 -0
  67. package/src/procedure.ts +283 -0
  68. package/src/rag-ingest-types.ts +52 -0
  69. package/src/rag-operations-types.ts +90 -0
  70. package/src/rag-schema.ts +94 -0
  71. package/src/reactive-mutation-types.ts +13 -0
  72. package/src/reactive-mutation.ts +115 -0
  73. package/src/reactive-query-types.ts +14 -0
  74. package/src/reactive-query.ts +48 -0
  75. package/src/reactive-realtime.ts +267 -0
  76. package/src/rls-db.ts +9 -4
  77. package/src/runtime-env-policy.ts +66 -0
  78. package/src/search-types.ts +91 -0
  79. package/src/server.ts +6 -2
  80. package/src/storage-metering.ts +35 -0
  81. package/src/storage-shared.ts +74 -0
  82. package/src/storage.ts +44 -53
  83. package/src/wake-app-result.ts +37 -0
  84. package/src/workflow-types.ts +16 -2
  85. package/src/workflow.ts +166 -12
  86. package/src/workflows-api.ts +82 -3
  87. package/src/__tests__/auth.test.ts +0 -118
  88. package/src/__tests__/crons.test.ts +0 -83
  89. package/src/__tests__/crud-codegen-integration.test.ts +0 -246
  90. package/src/__tests__/crud-owner-rls.test.ts +0 -387
  91. package/src/__tests__/crud.test.ts +0 -930
  92. package/src/__tests__/dist-exports.test.ts +0 -176
  93. package/src/__tests__/fixtures/basic/auth.ts +0 -32
  94. package/src/__tests__/fixtures/basic/drizzle.config.ts +0 -12
  95. package/src/__tests__/fixtures/basic/index.ts +0 -6
  96. package/src/__tests__/fixtures/basic/migrations/0000_last_warstar.sql +0 -75
  97. package/src/__tests__/fixtures/basic/migrations/meta/0000_snapshot.json +0 -497
  98. package/src/__tests__/fixtures/basic/migrations/meta/_journal.json +0 -13
  99. package/src/__tests__/fixtures/basic/schema.ts +0 -51
  100. package/src/__tests__/fixtures/basic/tasks.ts +0 -15
  101. package/src/__tests__/fixtures/common/auth-schema.ts +0 -67
  102. package/src/__tests__/helpers/basic-rls-fixture.ts +0 -135
  103. package/src/__tests__/helpers/pglite-migrations.ts +0 -32
  104. package/src/__tests__/helpers/pglite-rls-session.ts +0 -51
  105. package/src/__tests__/helpers/seed-like-fill.ts +0 -202
  106. package/src/__tests__/helpers/test-gencow-ctx-rls.ts +0 -50
  107. package/src/__tests__/httpaction.test.ts +0 -122
  108. package/src/__tests__/image-optimization.test.ts +0 -648
  109. package/src/__tests__/load.test.ts +0 -389
  110. package/src/__tests__/network-sim.test.ts +0 -319
  111. package/src/__tests__/reactive.test.ts +0 -479
  112. package/src/__tests__/retry.test.ts +0 -113
  113. package/src/__tests__/rls-crud-basic.test.ts +0 -317
  114. package/src/__tests__/rls-crud-no-owner-rls-pglite.test.ts +0 -117
  115. package/src/__tests__/rls-custom-mutation-handlers.test.ts +0 -142
  116. package/src/__tests__/rls-custom-query-handlers.test.ts +0 -128
  117. package/src/__tests__/rls-db-leased-connection.test.ts +0 -118
  118. package/src/__tests__/rls-session-and-policies.test.ts +0 -228
  119. package/src/__tests__/scheduler-durable-v2.test.ts +0 -288
  120. package/src/__tests__/scheduler-durable.test.ts +0 -173
  121. package/src/__tests__/scheduler-exec.test.ts +0 -328
  122. package/src/__tests__/scheduler.test.ts +0 -187
  123. package/src/__tests__/storage.test.ts +0 -334
  124. package/src/__tests__/tsconfig.json +0 -8
  125. package/src/__tests__/validator.test.ts +0 -323
  126. package/src/__tests__/workflow.test.ts +0 -606
  127. package/src/__tests__/ws-integration.test.ts +0 -309
  128. package/src/__tests__/ws-scale.test.ts +0 -241
  129. package/src/auth.ts +0 -155
  130. package/src/reactive.ts +0 -580
package/src/workflow.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { sql } from "drizzle-orm";
2
- import { mutation } from "./reactive.js";
3
- import type { MutationDef } from "./reactive.js";
2
+ import { mutation } from "./reactive-mutation.js";
3
+ import type { MutationDef } from "./reactive-mutation-types.js";
4
4
  import type {
5
5
  WorkflowDef,
6
6
  WorkflowDuration,
@@ -82,9 +82,137 @@ export function parseWorkflowDurationMs(raw: WorkflowDuration, label = "workflow
82
82
  return parseDurationString(raw, label);
83
83
  }
84
84
 
85
- function normalizeMaxDurationMs(maxDuration: WorkflowDuration | undefined): number {
85
+ function normalizeMaxDurationMs(maxDuration: WorkflowDuration | undefined, label = "workflow() maxDuration"): number {
86
86
  if (maxDuration == null) return DEFAULT_WORKFLOW_MAX_DURATION_MS;
87
- return parseWorkflowDurationMs(maxDuration, "workflow() maxDuration");
87
+ return parseWorkflowDurationMs(maxDuration, label);
88
+ }
89
+
90
+ function normalizeOptionalDurationMs(
91
+ duration: WorkflowDuration | undefined,
92
+ label: string,
93
+ ): number | null {
94
+ return duration == null ? null : parseWorkflowDurationMs(duration, label);
95
+ }
96
+
97
+ function normalizeConcurrency(concurrency: number | undefined): number | null {
98
+ if (concurrency == null) return null;
99
+ if (!Number.isFinite(concurrency) || concurrency <= 0) {
100
+ throw new Error(`workflow() concurrency must be a positive finite number, got "${concurrency}"`);
101
+ }
102
+ return Math.floor(concurrency);
103
+ }
104
+
105
+ function isMissingWorkflowV2SchemaError(error: unknown): boolean {
106
+ const code =
107
+ error && typeof error === "object" && "code" in error ? String((error as { code?: unknown }).code) : "";
108
+ const cause =
109
+ error && typeof error === "object" && "cause" in error ? (error as { cause?: unknown }).cause : null;
110
+ const causeCode =
111
+ cause && typeof cause === "object" && "code" in cause ? String((cause as { code?: unknown }).code) : "";
112
+ const message = error instanceof Error ? error.message : String(error);
113
+ return (
114
+ code === "42P01" ||
115
+ code === "42703" ||
116
+ causeCode === "42P01" ||
117
+ causeCode === "42703" ||
118
+ message.includes('relation "_gencow_workflow_runs_v2" does not exist') ||
119
+ message.includes("relation _gencow_workflow_runs_v2 does not exist") ||
120
+ message.includes('relation "_gencow_workflow_outbox_v2" does not exist') ||
121
+ message.includes("relation _gencow_workflow_outbox_v2 does not exist") ||
122
+ message.includes('column "max_active_duration_ms"') ||
123
+ message.includes('column "retry_count"') ||
124
+ message.includes('column "user_id"')
125
+ );
126
+ }
127
+
128
+ async function tryInsertWorkflowV2WakeOutbox(
129
+ db: { execute: (query: ReturnType<typeof sql>) => Promise<unknown> },
130
+ workflowId: string,
131
+ ): Promise<void> {
132
+ try {
133
+ await db.execute(sql`
134
+ INSERT INTO _gencow_workflow_outbox_v2 (
135
+ id,
136
+ run_id,
137
+ kind,
138
+ available_at,
139
+ status
140
+ )
141
+ VALUES (
142
+ ${`start:${workflowId}`},
143
+ ${workflowId},
144
+ 'wake_run',
145
+ NOW(),
146
+ 'pending'
147
+ )
148
+ ON CONFLICT (id) DO NOTHING
149
+ `);
150
+ } catch (error) {
151
+ if (!isMissingWorkflowV2SchemaError(error)) throw error;
152
+ }
153
+ }
154
+
155
+ async function tryInsertWorkflowV2Run(options: {
156
+ db: { execute: (query: ReturnType<typeof sql>) => Promise<unknown> };
157
+ workflowId: string;
158
+ workflowName: string;
159
+ workflowVersion?: string | null;
160
+ args: unknown;
161
+ userId: string | null;
162
+ maxActiveDurationMs: number;
163
+ lifecycleTimeoutMs: number | null;
164
+ maxRetries: number;
165
+ }): Promise<boolean> {
166
+ try {
167
+ await options.db.execute(sql`
168
+ INSERT INTO _gencow_workflow_runs_v2 (
169
+ id,
170
+ workflow_name,
171
+ workflow_version,
172
+ args_json,
173
+ user_id,
174
+ max_active_duration_ms,
175
+ lifecycle_deadline_at,
176
+ retry_count,
177
+ max_retries,
178
+ max_attempts
179
+ )
180
+ VALUES (
181
+ ${options.workflowId},
182
+ ${options.workflowName},
183
+ ${options.workflowVersion ?? null},
184
+ ${JSON.stringify(options.args)}::jsonb,
185
+ ${options.userId},
186
+ ${options.maxActiveDurationMs},
187
+ CASE
188
+ WHEN ${options.lifecycleTimeoutMs}::bigint IS NULL THEN NULL
189
+ ELSE NOW() + (${options.lifecycleTimeoutMs}::bigint * INTERVAL '1 millisecond')
190
+ END,
191
+ 0,
192
+ ${options.maxRetries},
193
+ ${options.maxRetries + 1}
194
+ )
195
+ `);
196
+ await tryInsertWorkflowV2WakeOutbox(options.db, options.workflowId);
197
+ return true;
198
+ } catch (error) {
199
+ if (isMissingWorkflowV2SchemaError(error)) return false;
200
+ throw error;
201
+ }
202
+ }
203
+
204
+ async function tryDeleteWorkflowV2Run(
205
+ db: { execute: (query: ReturnType<typeof sql>) => Promise<unknown> },
206
+ workflowId: string,
207
+ ): Promise<void> {
208
+ try {
209
+ await db.execute(sql`
210
+ DELETE FROM _gencow_workflow_runs_v2
211
+ WHERE id = ${workflowId}
212
+ `);
213
+ } catch (error) {
214
+ if (!isMissingWorkflowV2SchemaError(error)) throw error;
215
+ }
88
216
  }
89
217
 
90
218
  export function getWorkflowResumeActionName(name: string): string {
@@ -119,14 +247,25 @@ export function workflow<TSchema = any, TReturn = any>(
119
247
  ): MutationDef<TSchema, WorkflowStartResult> {
120
248
  registerWorkflowsApi();
121
249
 
122
- const maxDurationMs = normalizeMaxDurationMs(options.maxDuration);
250
+ const maxActiveDurationMs = normalizeMaxDurationMs(
251
+ options.maxActiveDuration ?? options.maxDuration,
252
+ options.maxActiveDuration == null ? "workflow() maxDuration" : "workflow() maxActiveDuration",
253
+ );
254
+ const lifecycleTimeoutMs = normalizeOptionalDurationMs(
255
+ options.lifecycleTimeout,
256
+ "workflow() lifecycleTimeout",
257
+ );
123
258
  const maxRetries = clampRetries(options.retries);
124
259
 
125
260
  const def: WorkflowDef<TSchema, TReturn> = {
126
261
  name,
127
262
  argsSchema: options.args,
128
263
  isPublic: options.public === true,
129
- maxDurationMs,
264
+ version: options.version,
265
+ maxDurationMs: maxActiveDurationMs,
266
+ maxActiveDurationMs,
267
+ lifecycleTimeoutMs,
268
+ concurrency: normalizeConcurrency(options.concurrency),
130
269
  maxRetries,
131
270
  handler: options.handler,
132
271
  };
@@ -159,16 +298,28 @@ export function workflow<TSchema = any, TReturn = any>(
159
298
  ${workflowId},
160
299
  ${name},
161
300
  ${JSON.stringify(persistedArgs)}::jsonb,
162
- ${realtimeToken},
163
- 'pending',
164
- 0,
165
- ${maxRetries},
166
- ${maxDurationMs},
167
- ${ownerId}
301
+ ${realtimeToken},
302
+ 'pending',
303
+ 0,
304
+ ${maxRetries},
305
+ ${maxActiveDurationMs},
306
+ ${ownerId}
168
307
  )
169
308
  `);
309
+ let insertedWorkflowV2 = false;
170
310
 
171
311
  try {
312
+ insertedWorkflowV2 = await tryInsertWorkflowV2Run({
313
+ db: ctx.unsafeDb,
314
+ workflowId,
315
+ workflowName: name,
316
+ workflowVersion: options.version ?? null,
317
+ args: persistedArgs,
318
+ userId: ownerId,
319
+ maxActiveDurationMs,
320
+ lifecycleTimeoutMs,
321
+ maxRetries,
322
+ });
172
323
  const scheduledJobId = ctx.scheduler.runAfter(0, resumeAction, { workflowId });
173
324
  return {
174
325
  id: workflowId,
@@ -181,6 +332,9 @@ export function workflow<TSchema = any, TReturn = any>(
181
332
  DELETE FROM _gencow_workflows
182
333
  WHERE id = ${workflowId}
183
334
  `);
335
+ if (insertedWorkflowV2) {
336
+ await tryDeleteWorkflowV2Run(ctx.unsafeDb, workflowId);
337
+ }
184
338
  throw error;
185
339
  }
186
340
  },
@@ -1,5 +1,6 @@
1
1
  import { sql } from "drizzle-orm";
2
- import { mutation, query } from "./reactive.js";
2
+ import { query } from "./reactive-query.js";
3
+ import { mutation } from "./reactive-mutation.js";
3
4
  import {
4
5
  createWorkflowRealtimeToken,
5
6
  deserializeWorkflowValue,
@@ -31,6 +32,7 @@ type WorkflowRow = {
31
32
  current_step: string | null;
32
33
  result: unknown;
33
34
  error: string | null;
35
+ error_code: string | null;
34
36
  retry_count: number;
35
37
  max_retries: number;
36
38
  max_duration_ms: number;
@@ -101,6 +103,7 @@ function mapWorkflowSummary(row: WorkflowRow): WorkflowSummary {
101
103
  derivedStatus: deriveWorkflowStatus(row.status, row.current_step),
102
104
  currentStep: row.current_step,
103
105
  error: row.error,
106
+ errorCode: row.error_code,
104
107
  retryCount: row.retry_count,
105
108
  maxRetries: row.max_retries,
106
109
  maxDurationMs: Number(row.max_duration_ms),
@@ -198,10 +201,77 @@ async function loadWorkflowSignalTarget(
198
201
  FROM _gencow_workflows
199
202
  WHERE id = ${workflowId}
200
203
  LIMIT 1
201
- `);
204
+ `);
202
205
  return rowsFromResult<WorkflowSignalTargetRow>(result)[0] ?? null;
203
206
  }
204
207
 
208
+ function isMissingWorkflowV2SignalSchemaError(error: unknown): boolean {
209
+ const code =
210
+ error && typeof error === "object" && "code" in error ? String((error as { code?: unknown }).code) : "";
211
+ const cause =
212
+ error && typeof error === "object" && "cause" in error ? (error as { cause?: unknown }).cause : null;
213
+ const causeCode =
214
+ cause && typeof cause === "object" && "code" in cause ? String((cause as { code?: unknown }).code) : "";
215
+ const message = error instanceof Error ? error.message : String(error);
216
+ return (
217
+ code === "42P01" ||
218
+ code === "42703" ||
219
+ causeCode === "42P01" ||
220
+ causeCode === "42703" ||
221
+ ((code === "23503" || causeCode === "23503") && message.includes("_gencow_workflow_signals_v2")) ||
222
+ message.includes('relation "_gencow_workflow_signals_v2" does not exist') ||
223
+ message.includes("relation _gencow_workflow_signals_v2 does not exist") ||
224
+ message.includes('relation "_gencow_workflow_runs_v2" does not exist') ||
225
+ message.includes("relation _gencow_workflow_runs_v2 does not exist")
226
+ );
227
+ }
228
+
229
+ async function tryRecordWorkflowV2Signal(options: {
230
+ db: WorkflowDbLike;
231
+ workflowId: string;
232
+ event: string;
233
+ payload: unknown;
234
+ }): Promise<boolean> {
235
+ try {
236
+ await options.db.execute(sql`
237
+ WITH inserted AS (
238
+ INSERT INTO _gencow_workflow_signals_v2 (
239
+ id,
240
+ run_id,
241
+ event_name,
242
+ payload_json,
243
+ idempotency_key
244
+ )
245
+ VALUES (
246
+ ${crypto.randomUUID()},
247
+ ${options.workflowId},
248
+ ${options.event},
249
+ ${JSON.stringify(options.payload)}::jsonb,
250
+ ${crypto.randomUUID()}
251
+ )
252
+ RETURNING run_id
253
+ )
254
+ UPDATE _gencow_workflow_runs_v2 run
255
+ SET
256
+ status = 'queued',
257
+ runnable_at = NOW(),
258
+ lease_owner = NULL,
259
+ lease_expires_at = NULL,
260
+ heartbeat_at = NULL,
261
+ updated_at = NOW()
262
+ FROM inserted
263
+ WHERE run.id = inserted.run_id
264
+ AND run.status = 'waiting'
265
+ AND run.completed_at IS NULL
266
+ AND run.cancel_requested_at IS NULL
267
+ `);
268
+ return true;
269
+ } catch (error) {
270
+ if (isMissingWorkflowV2SignalSchemaError(error)) return false;
271
+ throw error;
272
+ }
273
+ }
274
+
205
275
  export async function loadWorkflowSnapshot(
206
276
  db: WorkflowDbLike,
207
277
  workflowId: string,
@@ -219,6 +289,7 @@ export async function loadWorkflowSnapshot(
219
289
  current_step,
220
290
  result,
221
291
  error,
292
+ error_code,
222
293
  retry_count,
223
294
  max_retries,
224
295
  max_duration_ms,
@@ -326,9 +397,15 @@ export function registerWorkflowsApi(): void {
326
397
  ${crypto.randomUUID()},
327
398
  ${workflow.id},
328
399
  ${normalizedEvent},
329
- ${JSON.stringify(persistedPayload)}::jsonb
400
+ ${JSON.stringify(persistedPayload)}::jsonb
330
401
  )
331
402
  `);
403
+ await tryRecordWorkflowV2Signal({
404
+ db: ctx.unsafeDb,
405
+ workflowId: workflow.id,
406
+ event: normalizedEvent,
407
+ payload: persistedPayload,
408
+ });
332
409
 
333
410
  let scheduledJobId: string | null = null;
334
411
  if (workflow.status === "pending" && workflow.current_step?.startsWith("wait:")) {
@@ -372,6 +449,7 @@ export function registerWorkflowsApi(): void {
372
449
  current_step,
373
450
  result,
374
451
  error,
452
+ error_code,
375
453
  retry_count,
376
454
  max_retries,
377
455
  max_duration_ms,
@@ -393,6 +471,7 @@ export function registerWorkflowsApi(): void {
393
471
  current_step,
394
472
  result,
395
473
  error,
474
+ error_code,
396
475
  retry_count,
397
476
  max_retries,
398
477
  max_duration_ms,
@@ -1,118 +0,0 @@
1
- /**
2
- * packages/core/src/__tests__/auth.test.ts
3
- *
4
- * Tests for auth module — AuthCtx, defineAuth, auth-config.
5
- *
6
- * Run: bun test packages/core/src/__tests__/auth.test.ts
7
- */
8
-
9
- import { describe, it, expect } from "bun:test";
10
- import { defineAuth } from "../auth-config.js";
11
- import type { GencowAuthConfig } from "../auth-config.js";
12
-
13
- // ─── defineAuth() ───────────────────────────────────────
14
-
15
- describe("defineAuth()", () => {
16
- it("빈 설정 객체 반환", () => {
17
- const config = defineAuth({});
18
- expect(config).toEqual({});
19
- });
20
-
21
- it("emailVerification 설정이 그대로 반환된다", () => {
22
- const sendFn = async () => {};
23
- const config = defineAuth({
24
- emailVerification: {
25
- sendOnSignUp: true,
26
- requireEmailVerification: true,
27
- autoSignInAfterVerification: true,
28
- sendVerificationEmail: sendFn,
29
- },
30
- });
31
-
32
- expect(config.emailVerification?.sendOnSignUp).toBe(true);
33
- expect(config.emailVerification?.requireEmailVerification).toBe(true);
34
- expect(config.emailVerification?.autoSignInAfterVerification).toBe(true);
35
- expect(config.emailVerification?.sendVerificationEmail).toBe(sendFn);
36
- });
37
-
38
- it("부분 설정도 허용된다", () => {
39
- const config = defineAuth({
40
- emailVerification: {
41
- sendVerificationEmail: async () => {},
42
- },
43
- });
44
-
45
- expect(config.emailVerification?.sendOnSignUp).toBeUndefined();
46
- expect(config.emailVerification?.sendVerificationEmail).toBeDefined();
47
- });
48
- });
49
-
50
- // ─── AuthCtx interface 패턴 ─────────────────────────────
51
-
52
- describe("AuthCtx 패턴 (mock)", () => {
53
- // AuthCtx는 런타임에서 생성되므로, 인터페이스 수준에서 패턴 검증
54
-
55
- it("getUserIdentity — 비로그인 시 null", () => {
56
- const authCtx = {
57
- getUserIdentity: () => null,
58
- requireAuth: () => {
59
- throw new Error("Authentication required");
60
- },
61
- };
62
-
63
- expect(authCtx.getUserIdentity()).toBeNull();
64
- });
65
-
66
- it("getUserIdentity — 로그인 시 유저 반환", () => {
67
- const user = { id: "u1", email: "test@test.com", name: "Test" };
68
- const authCtx = {
69
- getUserIdentity: () => user,
70
- requireAuth: () => user,
71
- };
72
-
73
- expect(authCtx.getUserIdentity()).toEqual(user);
74
- expect(authCtx.getUserIdentity()!.id).toBe("u1");
75
- expect(authCtx.getUserIdentity()!.email).toBe("test@test.com");
76
- });
77
-
78
- it("requireAuth — 비로그인 시 에러 throw", () => {
79
- const authCtx = {
80
- getUserIdentity: () => null,
81
- requireAuth: () => {
82
- throw new Error("Authentication required");
83
- },
84
- };
85
-
86
- expect(() => authCtx.requireAuth()).toThrow("Authentication required");
87
- });
88
-
89
- it("requireAuth — 로그인 시 유저 반환 (throw 안 함)", () => {
90
- const user = { id: "u2", email: "auth@test.com" };
91
- const authCtx = {
92
- getUserIdentity: () => user,
93
- requireAuth: () => user,
94
- };
95
-
96
- expect(() => authCtx.requireAuth()).not.toThrow();
97
- expect(authCtx.requireAuth()).toEqual(user);
98
- });
99
- });
100
-
101
- // ─── Secure by Default 검증 ─────────────────────────────
102
-
103
- describe("Secure by Default — 기본 인증 필수", () => {
104
- // query/mutation의 isPublic 기본값은 reactive.test.ts에서 검증됨.
105
- // 여기서는 auth 엔드포인트 레벨 패턴만 확인.
106
-
107
- it("공개(public) 쿼리는 auth 없이 실행 가능해야 함", () => {
108
- // 이 테스트는 query({ public: true })의 isPublic 플래그 확인
109
- // reactive.test.ts의 "Secure by Default" 섹션과 연계
110
- const mockQueryDef = { isPublic: true, handler: async () => [] };
111
- expect(mockQueryDef.isPublic).toBe(true);
112
- });
113
-
114
- it("비공개 쿼리는 기본적으로 auth 필수", () => {
115
- const mockQueryDef = { isPublic: false, handler: async () => [] };
116
- expect(mockQueryDef.isPublic).toBe(false);
117
- });
118
- });
@@ -1,83 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
- import { cronJobs } from "../crons.js";
3
-
4
- describe("cronJobs 빌더", () => {
5
- test("interval — minutes → cron 패턴 변환", () => {
6
- const crons = cronJobs();
7
- crons.interval("test", { minutes: 15 }, "test.action");
8
- const jobs = crons.getJobs();
9
- expect(jobs).toHaveLength(1);
10
- expect(jobs[0].name).toBe("test");
11
- expect(jobs[0].pattern).toBe("*/15 * * * *");
12
- expect(jobs[0].action).toBe("test.action");
13
- });
14
-
15
- test("interval — hours → cron 패턴 변환", () => {
16
- const crons = cronJobs();
17
- crons.interval("hourly", { hours: 6 }, "cleanup");
18
- expect(crons.getJobs()[0].pattern).toBe("0 */6 * * *");
19
- });
20
-
21
- test("interval — seconds → 6자리 cron 패턴", () => {
22
- const crons = cronJobs();
23
- crons.interval("fast", { seconds: 30 }, "tick");
24
- expect(crons.getJobs()[0].pattern).toBe("*/30 * * * * *");
25
- });
26
-
27
- test("daily — 특정 시각", () => {
28
- const crons = cronJobs();
29
- crons.daily("report", { hour: 9, minute: 30 }, "reports.gen");
30
- const job = crons.getJobs()[0];
31
- expect(job.pattern).toBe("30 9 * * *");
32
- });
33
-
34
- test("daily — minute 기본값 0", () => {
35
- const crons = cronJobs();
36
- crons.daily("cleanup", { hour: 2 }, "admin.cleanup");
37
- expect(crons.getJobs()[0].pattern).toBe("0 2 * * *");
38
- });
39
-
40
- test("weekly — 요일 + 시각", () => {
41
- const crons = cronJobs();
42
- crons.weekly("monday-report", { dayOfWeek: 1, hour: 9 }, "reports.weekly");
43
- expect(crons.getJobs()[0].pattern).toBe("0 9 * * 1");
44
- });
45
-
46
- test("cron — 직접 패턴 지정", () => {
47
- const crons = cronJobs();
48
- crons.cron("custom", "0 */2 * * *", "custom.handler");
49
- expect(crons.getJobs()[0].pattern).toBe("0 */2 * * *");
50
- });
51
-
52
- test("체이닝 지원", () => {
53
- const crons = cronJobs();
54
- crons
55
- .interval("a", { minutes: 5 }, "a.action")
56
- .daily("b", { hour: 3 }, "b.action")
57
- .cron("c", "0 * * * *", "c.action");
58
- expect(crons.getJobs()).toHaveLength(3);
59
- });
60
-
61
- test("인라인 핸들러 지원", () => {
62
- const crons = cronJobs();
63
- const fn = async () => {
64
- /* noop */
65
- };
66
- crons.interval("inline", { minutes: 1 }, fn);
67
- expect(typeof crons.getJobs()[0].action).toBe("function");
68
- });
69
-
70
- test("getJobs는 복사본 반환 (원본 보호)", () => {
71
- const crons = cronJobs();
72
- crons.interval("test", { minutes: 1 }, "test");
73
- const jobs1 = crons.getJobs();
74
- const jobs2 = crons.getJobs();
75
- expect(jobs1).not.toBe(jobs2); // 서로 다른 참조
76
- expect(jobs1).toEqual(jobs2); // 내용은 동일
77
- });
78
-
79
- test("interval — 옵션 없으면 에러", () => {
80
- const crons = cronJobs();
81
- expect(() => crons.interval("bad", {}, "bad")).toThrow("minutes, hours, 또는 seconds");
82
- });
83
- });