@gencow/core 0.1.28 → 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.
- package/dist/auth-config.d.ts +92 -5
- package/dist/config.d.ts +107 -0
- package/dist/config.js +12 -0
- package/dist/context.d.ts +139 -0
- package/dist/context.js +3 -0
- package/dist/crud.d.ts +5 -5
- package/dist/crud.js +19 -35
- package/dist/document-types.d.ts +2 -2
- package/dist/http-action.d.ts +77 -0
- package/dist/http-action.js +41 -0
- package/dist/index.d.ts +21 -5
- package/dist/index.js +12 -3
- package/dist/platform-capacity-profile.d.ts +19 -0
- package/dist/platform-capacity-profile.js +94 -0
- package/dist/procedure.d.ts +58 -0
- package/dist/procedure.js +115 -0
- package/dist/rag-schema.d.ts +449 -540
- package/dist/reactive-mutation-types.d.ts +11 -0
- package/dist/reactive-mutation-types.js +1 -0
- package/dist/reactive-mutation.d.ts +51 -0
- package/dist/reactive-mutation.js +75 -0
- package/dist/reactive-query-types.d.ts +12 -0
- package/dist/reactive-query-types.js +1 -0
- package/dist/reactive-query.d.ts +14 -0
- package/dist/reactive-query.js +28 -0
- package/dist/reactive-realtime.d.ts +48 -0
- package/dist/reactive-realtime.js +236 -0
- package/dist/reactive.d.ts +16 -5
- package/dist/reactive.js +65 -0
- package/dist/runtime-env-policy.js +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/storage-metering.d.ts +13 -0
- package/dist/storage-metering.js +18 -0
- package/dist/storage.d.ts +3 -1
- package/dist/storage.js +11 -7
- package/dist/wake-app-result.d.ts +22 -0
- package/dist/wake-app-result.js +11 -0
- package/dist/workflow-types.d.ts +13 -1
- package/dist/workflow.d.ts +1 -1
- package/dist/workflow.js +136 -11
- package/dist/workflows-api.js +71 -3
- package/package.json +4 -1
- package/src/auth-config.ts +104 -3
- package/src/config.ts +119 -0
- package/src/context.ts +152 -0
- package/src/crud.ts +18 -35
- package/src/document-types.ts +9 -2
- package/src/http-action.ts +101 -0
- package/src/index.ts +77 -19
- package/src/platform-capacity-profile.ts +114 -0
- package/src/procedure.ts +283 -0
- package/src/reactive-mutation-types.ts +13 -0
- package/src/reactive-mutation.ts +115 -0
- package/src/reactive-query-types.ts +14 -0
- package/src/reactive-query.ts +48 -0
- package/src/reactive-realtime.ts +267 -0
- package/src/runtime-env-policy.ts +1 -1
- package/src/server.ts +6 -1
- package/src/storage-metering.ts +35 -0
- package/src/storage.ts +14 -6
- package/src/wake-app-result.ts +37 -0
- package/src/workflow-types.ts +13 -1
- package/src/workflow.ts +166 -12
- package/src/workflows-api.ts +82 -3
- package/src/reactive.ts +0 -593
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
export type PlatformCapacityProfileName = "prod" | "dev" | "local";
|
|
2
|
+
|
|
3
|
+
export interface PlatformCapacityPreset {
|
|
4
|
+
profile: PlatformCapacityProfileName;
|
|
5
|
+
maxConcurrentRunning: number | null;
|
|
6
|
+
maxConcurrentWake: number | null;
|
|
7
|
+
maxWakeQueueMs: number;
|
|
8
|
+
minAvailableRamMB: number | null;
|
|
9
|
+
evictionThresholdMB: number | null;
|
|
10
|
+
sleepTimeoutMinutes: number;
|
|
11
|
+
sleepMaxPerCycle: number;
|
|
12
|
+
warmPoolMinIdle: number;
|
|
13
|
+
warmPoolMax: number;
|
|
14
|
+
deployCandidateReserve: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const PLATFORM_CAPACITY_PROFILE_ENV = "GENCOW_CAPACITY_PROFILE";
|
|
18
|
+
|
|
19
|
+
export const PLATFORM_CAPACITY_ENV_KEYS = [
|
|
20
|
+
PLATFORM_CAPACITY_PROFILE_ENV,
|
|
21
|
+
"MAX_CONCURRENT_RUNNING",
|
|
22
|
+
"MAX_CONCURRENT_WAKE",
|
|
23
|
+
"MAX_WAKE_QUEUE_MS",
|
|
24
|
+
"MIN_AVAILABLE_RAM_MB",
|
|
25
|
+
"WAKE_RETRY_AFTER_SEC",
|
|
26
|
+
"EVICTION_THRESHOLD_MB",
|
|
27
|
+
"GENCOW_SLEEP_TIMEOUT_MINUTES",
|
|
28
|
+
"GENCOW_SLEEP_MAX_PER_CYCLE",
|
|
29
|
+
"WARM_POOL_MIN_IDLE",
|
|
30
|
+
"WARM_POOL_MAX",
|
|
31
|
+
"DEPLOY_CANDIDATE_RESERVE",
|
|
32
|
+
"GENCOW_APP_PORT_RANGE",
|
|
33
|
+
"COWBOX_WARM_POOL_PORT_RANGE",
|
|
34
|
+
] as const;
|
|
35
|
+
|
|
36
|
+
export const PLATFORM_CAPACITY_PRESETS: Record<PlatformCapacityProfileName, PlatformCapacityPreset> = Object.freeze({
|
|
37
|
+
prod: Object.freeze({
|
|
38
|
+
profile: "prod",
|
|
39
|
+
maxConcurrentRunning: 600,
|
|
40
|
+
maxConcurrentWake: 20,
|
|
41
|
+
maxWakeQueueMs: 5000,
|
|
42
|
+
minAvailableRamMB: 8192,
|
|
43
|
+
evictionThresholdMB: 16384,
|
|
44
|
+
sleepTimeoutMinutes: 15,
|
|
45
|
+
sleepMaxPerCycle: 50,
|
|
46
|
+
warmPoolMinIdle: 10,
|
|
47
|
+
warmPoolMax: 650,
|
|
48
|
+
deployCandidateReserve: 40,
|
|
49
|
+
}),
|
|
50
|
+
dev: Object.freeze({
|
|
51
|
+
profile: "dev",
|
|
52
|
+
maxConcurrentRunning: 60,
|
|
53
|
+
maxConcurrentWake: 5,
|
|
54
|
+
maxWakeQueueMs: 3000,
|
|
55
|
+
minAvailableRamMB: 2048,
|
|
56
|
+
evictionThresholdMB: 4096,
|
|
57
|
+
sleepTimeoutMinutes: 10,
|
|
58
|
+
sleepMaxPerCycle: 20,
|
|
59
|
+
warmPoolMinIdle: 3,
|
|
60
|
+
warmPoolMax: 70,
|
|
61
|
+
deployCandidateReserve: 5,
|
|
62
|
+
}),
|
|
63
|
+
local: Object.freeze({
|
|
64
|
+
profile: "local",
|
|
65
|
+
maxConcurrentRunning: null,
|
|
66
|
+
maxConcurrentWake: null,
|
|
67
|
+
maxWakeQueueMs: 5000,
|
|
68
|
+
minAvailableRamMB: null,
|
|
69
|
+
evictionThresholdMB: null,
|
|
70
|
+
sleepTimeoutMinutes: 30,
|
|
71
|
+
sleepMaxPerCycle: 10,
|
|
72
|
+
warmPoolMinIdle: 3,
|
|
73
|
+
warmPoolMax: 10,
|
|
74
|
+
deployCandidateReserve: 0,
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
function normalizeDomain(value: string | undefined): string {
|
|
79
|
+
return (value || "")
|
|
80
|
+
.trim()
|
|
81
|
+
.toLowerCase()
|
|
82
|
+
.replace(/^https?:\/\//, "")
|
|
83
|
+
.replace(/\/.*$/, "");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function detectPlatformCapacityProfile(
|
|
87
|
+
env: Record<string, string | undefined> = process.env,
|
|
88
|
+
): PlatformCapacityProfileName {
|
|
89
|
+
const explicit = env[PLATFORM_CAPACITY_PROFILE_ENV]?.trim().toLowerCase();
|
|
90
|
+
if (explicit) {
|
|
91
|
+
if (["prod", "production", "128gb"].includes(explicit)) return "prod";
|
|
92
|
+
if (["dev", "development", "16gb"].includes(explicit)) return "dev";
|
|
93
|
+
if (["local", "test", "disabled"].includes(explicit)) return "local";
|
|
94
|
+
throw new Error(`Invalid ${PLATFORM_CAPACITY_PROFILE_ENV}: ${explicit}. Expected prod, dev, or local.`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const baseDomain = normalizeDomain(env.BASE_DOMAIN);
|
|
98
|
+
if (baseDomain === "gencow.dev") return "dev";
|
|
99
|
+
if (baseDomain === "gencow.app") return "prod";
|
|
100
|
+
|
|
101
|
+
const platformUrl = normalizeDomain(env.GENCOW_PLATFORM_URL);
|
|
102
|
+
if (platformUrl === "gencow.dev") return "dev";
|
|
103
|
+
if (platformUrl === "gencow.app") return "prod";
|
|
104
|
+
|
|
105
|
+
if (env.NODE_ENV === "production" && env.IS_PLATFORM === "true") return "prod";
|
|
106
|
+
return "local";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function resolvePlatformCapacityPreset(
|
|
110
|
+
env: Record<string, string | undefined> = process.env,
|
|
111
|
+
): PlatformCapacityPreset {
|
|
112
|
+
const profile = detectPlatformCapacityProfile(env);
|
|
113
|
+
return { ...PLATFORM_CAPACITY_PRESETS[profile] };
|
|
114
|
+
}
|
package/src/procedure.ts
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
|
+
import type { GencowCtx } from "./context.js";
|
|
3
|
+
|
|
4
|
+
type InferInputSchema<TSchema> = TSchema extends { "~standard": { types: { input: infer TInput } } }
|
|
5
|
+
? TInput
|
|
6
|
+
: TSchema extends StandardSchemaV1
|
|
7
|
+
? StandardSchemaV1.InferInput<TSchema>
|
|
8
|
+
: unknown;
|
|
9
|
+
|
|
10
|
+
type InferOutputSchema<TSchema> = TSchema extends { "~standard": { types: { output: infer TOutput } } }
|
|
11
|
+
? TOutput
|
|
12
|
+
: TSchema extends { _output: infer TOutput }
|
|
13
|
+
? TOutput
|
|
14
|
+
: TSchema extends StandardSchemaV1
|
|
15
|
+
? StandardSchemaV1.InferOutput<TSchema>
|
|
16
|
+
: unknown;
|
|
17
|
+
|
|
18
|
+
type NextFn<TContext, TInput, TOutput> = (overrides?: {
|
|
19
|
+
context?: TContext;
|
|
20
|
+
input?: TInput;
|
|
21
|
+
}) => Promise<TOutput>;
|
|
22
|
+
|
|
23
|
+
export type GencowProcedureMiddleware<TContext, TInput, TOutput> = (options: {
|
|
24
|
+
context: TContext;
|
|
25
|
+
input: TInput;
|
|
26
|
+
next: NextFn<TContext, TInput, TOutput>;
|
|
27
|
+
}) => Promise<TOutput> | TOutput;
|
|
28
|
+
|
|
29
|
+
export type GencowProcedureHandler<TContext, TInput, TOutput> = (options: {
|
|
30
|
+
context: TContext;
|
|
31
|
+
input: TInput;
|
|
32
|
+
}) => Promise<TOutput> | TOutput;
|
|
33
|
+
|
|
34
|
+
export interface GencowProcedureDef<
|
|
35
|
+
TKind extends "query" | "mutation" = "query" | "mutation",
|
|
36
|
+
TContext = any,
|
|
37
|
+
TInput = unknown,
|
|
38
|
+
TOutput = unknown,
|
|
39
|
+
> {
|
|
40
|
+
kind: TKind;
|
|
41
|
+
name: string;
|
|
42
|
+
/** When false (default), RPC requires an authenticated user. */
|
|
43
|
+
isPublic: boolean;
|
|
44
|
+
middlewares: readonly GencowProcedureMiddleware<TContext, TInput, TOutput>[];
|
|
45
|
+
inputSchema?: StandardSchemaV1;
|
|
46
|
+
outputSchema?: StandardSchemaV1;
|
|
47
|
+
inputValidationIndex: number;
|
|
48
|
+
outputValidationIndex: number;
|
|
49
|
+
handler: (context: TContext, input: TInput) => Promise<TOutput>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface GencowProcedureBuilder<
|
|
53
|
+
TKind extends "query" | "mutation" = "query" | "mutation",
|
|
54
|
+
TContext = any,
|
|
55
|
+
TInput = unknown,
|
|
56
|
+
TOutput = unknown,
|
|
57
|
+
> {
|
|
58
|
+
name(name: string): GencowProcedureBuilder<TKind, TContext, TInput, TOutput>;
|
|
59
|
+
/**
|
|
60
|
+
* Allow calling this procedure without authentication (like `query("x", { public: true, ... })`).
|
|
61
|
+
* Default is authenticated-only.
|
|
62
|
+
*/
|
|
63
|
+
allowPublic(): GencowProcedureBuilder<TKind, TContext, TInput, TOutput>;
|
|
64
|
+
use(
|
|
65
|
+
middleware: GencowProcedureMiddleware<TContext, TInput, TOutput>,
|
|
66
|
+
): GencowProcedureBuilder<TKind, TContext, TInput, TOutput>;
|
|
67
|
+
input<TNextSchema extends StandardSchemaV1>(
|
|
68
|
+
schema: TNextSchema,
|
|
69
|
+
): GencowProcedureBuilder<TKind, TContext, InferInputSchema<TNextSchema>, TOutput>;
|
|
70
|
+
output<TNextSchema extends StandardSchemaV1>(
|
|
71
|
+
schema: TNextSchema,
|
|
72
|
+
): GencowProcedureBuilder<TKind, TContext, TInput, InferOutputSchema<TNextSchema>>;
|
|
73
|
+
handler(
|
|
74
|
+
handler: GencowProcedureHandler<TContext, TInput, TOutput>,
|
|
75
|
+
): GencowProcedureDef<TKind, TContext, TInput, TOutput>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function hasStandardSchema(schema: unknown): schema is StandardSchemaV1 {
|
|
79
|
+
return (
|
|
80
|
+
!!schema &&
|
|
81
|
+
typeof schema === "object" &&
|
|
82
|
+
"~standard" in schema &&
|
|
83
|
+
!!(schema as any)["~standard"] &&
|
|
84
|
+
typeof (schema as any)["~standard"].validate === "function"
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function validateWithSchema<TOutput>(schema: unknown, value: unknown): Promise<TOutput> {
|
|
89
|
+
if (!schema) return value as TOutput;
|
|
90
|
+
|
|
91
|
+
if (hasStandardSchema(schema)) {
|
|
92
|
+
const result = (await schema["~standard"].validate(value)) as
|
|
93
|
+
| { value: TOutput }
|
|
94
|
+
| { issues: readonly { message: string }[] };
|
|
95
|
+
if ("issues" in result && result.issues.length > 0) {
|
|
96
|
+
throw new Error(result.issues[0]?.message ?? "Validation failed");
|
|
97
|
+
}
|
|
98
|
+
if ("value" in result) {
|
|
99
|
+
return result.value as TOutput;
|
|
100
|
+
}
|
|
101
|
+
throw new Error("Validation failed");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
throw new Error("procQuery/procMutation only supports Standard Schema v1 validators");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function composeMiddlewares<TContext, TInput, TOutput>(
|
|
108
|
+
middlewares: Array<GencowProcedureMiddleware<TContext, TInput, TOutput>>,
|
|
109
|
+
handler: GencowProcedureHandler<TContext, TInput, TOutput>,
|
|
110
|
+
inputSchema: StandardSchemaV1 | undefined,
|
|
111
|
+
outputSchema: StandardSchemaV1 | undefined,
|
|
112
|
+
inputValidationIndex: number,
|
|
113
|
+
outputValidationIndex: number,
|
|
114
|
+
): (context: TContext, input: TInput) => Promise<TOutput> {
|
|
115
|
+
return async (initialContext, initialInput) => {
|
|
116
|
+
let idx = -1;
|
|
117
|
+
const boundedInputValidationIndex = Math.min(Math.max(0, inputValidationIndex), middlewares.length);
|
|
118
|
+
const boundedOutputValidationIndex = Math.min(Math.max(0, outputValidationIndex), middlewares.length);
|
|
119
|
+
|
|
120
|
+
const run = async (currentIdx: number, context: TContext, input: TInput): Promise<TOutput> => {
|
|
121
|
+
if (currentIdx <= idx) {
|
|
122
|
+
throw new Error("next() called multiple times in the same middleware");
|
|
123
|
+
}
|
|
124
|
+
idx = currentIdx;
|
|
125
|
+
let currentInput = input;
|
|
126
|
+
|
|
127
|
+
if (currentIdx === boundedInputValidationIndex) {
|
|
128
|
+
currentInput = (await validateWithSchema(inputSchema, currentInput)) as TInput;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (currentIdx === middlewares.length) {
|
|
132
|
+
const output = await handler({ context, input: currentInput });
|
|
133
|
+
if (currentIdx === boundedOutputValidationIndex) {
|
|
134
|
+
return await validateWithSchema<TOutput>(outputSchema, output);
|
|
135
|
+
}
|
|
136
|
+
return output;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const middleware = middlewares[currentIdx];
|
|
140
|
+
const output = await middleware({
|
|
141
|
+
context,
|
|
142
|
+
input: currentInput,
|
|
143
|
+
next: async (overrides) => {
|
|
144
|
+
return await run(
|
|
145
|
+
currentIdx + 1,
|
|
146
|
+
(overrides?.context ?? context) as TContext,
|
|
147
|
+
(overrides?.input ?? currentInput) as TInput,
|
|
148
|
+
);
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (currentIdx === boundedOutputValidationIndex) {
|
|
153
|
+
return await validateWithSchema<TOutput>(outputSchema, output);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return output;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
return await run(0, initialContext, initialInput);
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
class GencowProcedureBuilderImpl<
|
|
164
|
+
TKind extends "query" | "mutation",
|
|
165
|
+
TContext = any,
|
|
166
|
+
TInput = unknown,
|
|
167
|
+
TOutput = unknown,
|
|
168
|
+
> implements GencowProcedureBuilder<TKind, TContext, TInput, TOutput> {
|
|
169
|
+
constructor(
|
|
170
|
+
private readonly kind: TKind,
|
|
171
|
+
private readonly procedureName?: string,
|
|
172
|
+
private readonly middlewares: Array<GencowProcedureMiddleware<TContext, TInput, TOutput>> = [],
|
|
173
|
+
private readonly inputSchema?: StandardSchemaV1,
|
|
174
|
+
private readonly outputSchema?: StandardSchemaV1,
|
|
175
|
+
private readonly inputValidationIndex = -1,
|
|
176
|
+
private readonly outputValidationIndex = -1,
|
|
177
|
+
private readonly isPublic = false,
|
|
178
|
+
) {}
|
|
179
|
+
|
|
180
|
+
name(name: string) {
|
|
181
|
+
return new GencowProcedureBuilderImpl<TKind, TContext, TInput, TOutput>(
|
|
182
|
+
this.kind,
|
|
183
|
+
name,
|
|
184
|
+
this.middlewares,
|
|
185
|
+
this.inputSchema,
|
|
186
|
+
this.outputSchema,
|
|
187
|
+
this.inputValidationIndex,
|
|
188
|
+
this.outputValidationIndex,
|
|
189
|
+
this.isPublic,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
allowPublic() {
|
|
194
|
+
return new GencowProcedureBuilderImpl<TKind, TContext, TInput, TOutput>(
|
|
195
|
+
this.kind,
|
|
196
|
+
this.procedureName,
|
|
197
|
+
this.middlewares,
|
|
198
|
+
this.inputSchema,
|
|
199
|
+
this.outputSchema,
|
|
200
|
+
this.inputValidationIndex,
|
|
201
|
+
this.outputValidationIndex,
|
|
202
|
+
true,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
use(middleware: GencowProcedureMiddleware<TContext, TInput, TOutput>) {
|
|
207
|
+
return new GencowProcedureBuilderImpl<TKind, TContext, TInput, TOutput>(
|
|
208
|
+
this.kind,
|
|
209
|
+
this.procedureName,
|
|
210
|
+
[...this.middlewares, middleware],
|
|
211
|
+
this.inputSchema,
|
|
212
|
+
this.outputSchema,
|
|
213
|
+
this.inputValidationIndex,
|
|
214
|
+
this.outputValidationIndex,
|
|
215
|
+
this.isPublic,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
input<TNextSchema extends StandardSchemaV1>(schema: TNextSchema) {
|
|
220
|
+
const nextInputValidationIndex = this.middlewares.length < 0 ? 0 : this.middlewares.length;
|
|
221
|
+
|
|
222
|
+
return new GencowProcedureBuilderImpl<TKind, TContext, InferInputSchema<TNextSchema>, TOutput>(
|
|
223
|
+
this.kind,
|
|
224
|
+
this.procedureName,
|
|
225
|
+
this.middlewares as Array<GencowProcedureMiddleware<TContext, InferInputSchema<TNextSchema>, TOutput>>,
|
|
226
|
+
schema,
|
|
227
|
+
this.outputSchema,
|
|
228
|
+
nextInputValidationIndex,
|
|
229
|
+
this.outputValidationIndex,
|
|
230
|
+
this.isPublic,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
output<TNextSchema extends StandardSchemaV1>(schema: TNextSchema) {
|
|
235
|
+
const nextOutputValidationIndex = this.middlewares.length < 0 ? 0 : this.middlewares.length;
|
|
236
|
+
|
|
237
|
+
return new GencowProcedureBuilderImpl<TKind, TContext, TInput, InferOutputSchema<TNextSchema>>(
|
|
238
|
+
this.kind,
|
|
239
|
+
this.procedureName,
|
|
240
|
+
this.middlewares as Array<GencowProcedureMiddleware<TContext, TInput, InferOutputSchema<TNextSchema>>>,
|
|
241
|
+
this.inputSchema,
|
|
242
|
+
schema,
|
|
243
|
+
this.inputValidationIndex,
|
|
244
|
+
nextOutputValidationIndex,
|
|
245
|
+
this.isPublic,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
handler(
|
|
250
|
+
handler: GencowProcedureHandler<TContext, TInput, TOutput>,
|
|
251
|
+
): GencowProcedureDef<TKind, TContext, TInput, TOutput> {
|
|
252
|
+
if (!this.procedureName) {
|
|
253
|
+
throw new Error("Procedure name is required. Call .name(...) before .handler(...)");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const run = composeMiddlewares(
|
|
257
|
+
this.middlewares,
|
|
258
|
+
handler,
|
|
259
|
+
this.inputSchema,
|
|
260
|
+
this.outputSchema,
|
|
261
|
+
this.inputValidationIndex,
|
|
262
|
+
this.outputValidationIndex,
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
kind: this.kind,
|
|
267
|
+
name: this.procedureName,
|
|
268
|
+
isPublic: this.isPublic,
|
|
269
|
+
middlewares: [...this.middlewares],
|
|
270
|
+
inputSchema: this.inputSchema,
|
|
271
|
+
outputSchema: this.outputSchema,
|
|
272
|
+
inputValidationIndex: this.inputValidationIndex,
|
|
273
|
+
outputValidationIndex: this.outputValidationIndex,
|
|
274
|
+
handler: run,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export const procQuery: GencowProcedureBuilder<"query", GencowCtx> =
|
|
280
|
+
new GencowProcedureBuilderImpl<"query", GencowCtx>("query");
|
|
281
|
+
|
|
282
|
+
export const procMutation: GencowProcedureBuilder<"mutation", GencowCtx> =
|
|
283
|
+
new GencowProcedureBuilderImpl<"mutation", GencowCtx>("mutation");
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { InferArgs } from "./v.js";
|
|
2
|
+
import type { GencowCtx } from "./context.js";
|
|
3
|
+
|
|
4
|
+
export type MutationHandler<TArgs = any, TReturn = any> = (ctx: GencowCtx, args: TArgs) => Promise<TReturn>;
|
|
5
|
+
|
|
6
|
+
export interface MutationDef<TSchema = any, TReturn = any> {
|
|
7
|
+
handler: MutationHandler<InferArgs<TSchema>, TReturn>;
|
|
8
|
+
argsSchema?: TSchema;
|
|
9
|
+
/** true = 인증 없이 접근 가능, false(기본) = auth 필수 (Secure by Default) */
|
|
10
|
+
isPublic: boolean;
|
|
11
|
+
_args?: InferArgs<TSchema>;
|
|
12
|
+
_return?: TReturn;
|
|
13
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { type InferArgs } from "./v.js";
|
|
2
|
+
import type { MutationDef, MutationHandler } from "./reactive-mutation-types.js";
|
|
3
|
+
|
|
4
|
+
// ─── mutation registry (globalThis) ─────────────────────
|
|
5
|
+
// globalThis shared singleton — dual-bundle safe (see analysis-dual-module-registry.md).
|
|
6
|
+
|
|
7
|
+
declare global {
|
|
8
|
+
var __gencow_mutationRegistry: (MutationDef<any, any> & { name: string })[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (!globalThis.__gencow_mutationRegistry) globalThis.__gencow_mutationRegistry = [];
|
|
12
|
+
|
|
13
|
+
export const mutationRegistry = globalThis.__gencow_mutationRegistry;
|
|
14
|
+
|
|
15
|
+
let mutationCounter = 0;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* mutation — 데이터 변경 함수를 선언적으로 등록합니다.
|
|
19
|
+
*
|
|
20
|
+
* 3가지 호출 방식 지원 (query와 동일한 패턴 우선):
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* // ✅ 권장: query와 동일한 (name, def) 패턴
|
|
25
|
+
* mutation("tasks.create", {
|
|
26
|
+
* args: { title: v.string() },
|
|
27
|
+
* handler: async (ctx, args) => { ... },
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* // ✅ 객체 스타일 (하위 호환)
|
|
31
|
+
* mutation({
|
|
32
|
+
* name: "tasks.create",
|
|
33
|
+
* handler: async (ctx) => { ... },
|
|
34
|
+
* });
|
|
35
|
+
*
|
|
36
|
+
* // ⚠️ Legacy 배열 스타일 (비권장)
|
|
37
|
+
* mutation(["tasks.list"], handler, "tasks.create");
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* @note invalidates 필드는 deprecated — 전달해도 무시됩니다.
|
|
41
|
+
* 리얼타임 UI 갱신에는 ctx.realtime.emit() 또는 ctx.realtime.refresh()를 사용하세요.
|
|
42
|
+
*/
|
|
43
|
+
export function mutation<TSchema = any, TReturn = any>(
|
|
44
|
+
nameOrInvalidatesOrDef:
|
|
45
|
+
| string
|
|
46
|
+
| string[]
|
|
47
|
+
| {
|
|
48
|
+
name?: string;
|
|
49
|
+
args?: TSchema;
|
|
50
|
+
public?: boolean;
|
|
51
|
+
invalidates?: string[];
|
|
52
|
+
handler: MutationHandler<InferArgs<TSchema>, TReturn>;
|
|
53
|
+
},
|
|
54
|
+
handlerOrDef?:
|
|
55
|
+
| MutationHandler<InferArgs<TSchema>, TReturn>
|
|
56
|
+
| {
|
|
57
|
+
invalidates?: string[];
|
|
58
|
+
args?: TSchema;
|
|
59
|
+
public?: boolean;
|
|
60
|
+
handler: MutationHandler<InferArgs<TSchema>, TReturn>;
|
|
61
|
+
},
|
|
62
|
+
name?: string,
|
|
63
|
+
): MutationDef<TSchema, TReturn> {
|
|
64
|
+
let argsSchema: TSchema | undefined;
|
|
65
|
+
let actualHandler: MutationHandler<InferArgs<TSchema>, TReturn>;
|
|
66
|
+
let mutName: string;
|
|
67
|
+
let isPublic = false;
|
|
68
|
+
|
|
69
|
+
if (typeof nameOrInvalidatesOrDef === "string") {
|
|
70
|
+
// New primary style: mutation("name", { args?, public?, handler })
|
|
71
|
+
mutName = nameOrInvalidatesOrDef;
|
|
72
|
+
const def = handlerOrDef as {
|
|
73
|
+
args?: TSchema;
|
|
74
|
+
public?: boolean;
|
|
75
|
+
handler: MutationHandler<InferArgs<TSchema>, TReturn>;
|
|
76
|
+
};
|
|
77
|
+
actualHandler = def.handler;
|
|
78
|
+
argsSchema = def.args;
|
|
79
|
+
isPublic = def.public === true;
|
|
80
|
+
} else if (Array.isArray(nameOrInvalidatesOrDef)) {
|
|
81
|
+
// Legacy style: mutation([...], handler, "name") — invalidates ignored
|
|
82
|
+
actualHandler = handlerOrDef as MutationHandler<InferArgs<TSchema>, TReturn>;
|
|
83
|
+
mutName = name || `mutation_${++mutationCounter}`;
|
|
84
|
+
} else {
|
|
85
|
+
// Object style: mutation({ name?, args?, public?, handler })
|
|
86
|
+
actualHandler = nameOrInvalidatesOrDef.handler;
|
|
87
|
+
argsSchema = nameOrInvalidatesOrDef.args;
|
|
88
|
+
isPublic = nameOrInvalidatesOrDef.public === true;
|
|
89
|
+
mutName =
|
|
90
|
+
nameOrInvalidatesOrDef.name ||
|
|
91
|
+
(typeof name === "string" ? name : "") ||
|
|
92
|
+
`mutation_${++mutationCounter}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 이름 미지정 시 경고 — 디버깅 지원
|
|
96
|
+
if (mutName.startsWith("mutation_")) {
|
|
97
|
+
console.warn(
|
|
98
|
+
`[gencow] mutation registered without explicit name → "${mutName}". ` +
|
|
99
|
+
`Use mutation("myMutation", { handler }) for better debugging.`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const def: MutationDef<TSchema, TReturn> & { name: string } = {
|
|
104
|
+
name: mutName,
|
|
105
|
+
handler: actualHandler,
|
|
106
|
+
argsSchema,
|
|
107
|
+
isPublic,
|
|
108
|
+
};
|
|
109
|
+
mutationRegistry.push(def);
|
|
110
|
+
return def;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function getRegisteredMutations(): (MutationDef & { name: string })[] {
|
|
114
|
+
return [...mutationRegistry];
|
|
115
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { InferArgs } from "./v.js";
|
|
2
|
+
import type { GencowCtx } from "./context.js";
|
|
3
|
+
|
|
4
|
+
export type QueryHandler<TArgs = any, TReturn = any> = (ctx: GencowCtx, args: TArgs) => Promise<TReturn>;
|
|
5
|
+
|
|
6
|
+
export interface QueryDef<TSchema = any, TReturn = any> {
|
|
7
|
+
key: string;
|
|
8
|
+
handler: QueryHandler<InferArgs<TSchema>, TReturn>;
|
|
9
|
+
argsSchema?: TSchema;
|
|
10
|
+
/** true = 인증 없이 접근 가능, false(기본) = auth 필수 (Secure by Default) */
|
|
11
|
+
isPublic: boolean;
|
|
12
|
+
_args?: InferArgs<TSchema>;
|
|
13
|
+
_return?: TReturn;
|
|
14
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type InferArgs } from "./v.js";
|
|
2
|
+
import type { QueryDef, QueryHandler } from "./reactive-query-types.js";
|
|
3
|
+
|
|
4
|
+
// ─── query registry (globalThis) ─────────────────────────
|
|
5
|
+
// globalThis shared singleton — dual-bundle safe (see analysis-dual-module-registry.md).
|
|
6
|
+
|
|
7
|
+
declare global {
|
|
8
|
+
var __gencow_queryRegistry: Map<string, QueryDef<any, any>>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (!globalThis.__gencow_queryRegistry) globalThis.__gencow_queryRegistry = new Map();
|
|
12
|
+
|
|
13
|
+
export const queryRegistry = globalThis.__gencow_queryRegistry;
|
|
14
|
+
|
|
15
|
+
export function query<TSchema = any, TReturn = any>(
|
|
16
|
+
key: string,
|
|
17
|
+
handlerOrDef:
|
|
18
|
+
| QueryHandler<InferArgs<TSchema>, TReturn>
|
|
19
|
+
| { args?: TSchema; public?: boolean; handler: QueryHandler<InferArgs<TSchema>, TReturn> },
|
|
20
|
+
): QueryDef<TSchema, TReturn> {
|
|
21
|
+
let handler: QueryHandler<InferArgs<TSchema>, TReturn>;
|
|
22
|
+
let argsSchema: TSchema | undefined;
|
|
23
|
+
let isPublic = false;
|
|
24
|
+
|
|
25
|
+
if (typeof handlerOrDef === "function") {
|
|
26
|
+
handler = handlerOrDef;
|
|
27
|
+
} else {
|
|
28
|
+
handler = handlerOrDef.handler;
|
|
29
|
+
argsSchema = handlerOrDef.args;
|
|
30
|
+
isPublic = handlerOrDef.public === true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const def: QueryDef<TSchema, TReturn> = { key, handler, argsSchema, isPublic };
|
|
34
|
+
queryRegistry.set(key, def);
|
|
35
|
+
return def;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getQueryHandler(key: string): QueryHandler | undefined {
|
|
39
|
+
return queryRegistry.get(key)?.handler;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getQueryDef(key: string): QueryDef | undefined {
|
|
43
|
+
return queryRegistry.get(key);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getRegisteredQueries(): string[] {
|
|
47
|
+
return Array.from(queryRegistry.keys());
|
|
48
|
+
}
|