@rigkit/engine 0.1.8
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/README.md +5 -0
- package/package.json +34 -0
- package/src/authoring.ts +334 -0
- package/src/db/index.ts +157 -0
- package/src/db/schema/core.ts +75 -0
- package/src/db/schema/index.ts +7 -0
- package/src/engine.test.ts +866 -0
- package/src/engine.ts +2059 -0
- package/src/env-file.ts +52 -0
- package/src/hash.ts +21 -0
- package/src/index.ts +34 -0
- package/src/provider/types.ts +139 -0
- package/src/state.ts +318 -0
- package/src/types.ts +604 -0
- package/src/version.ts +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# @rigkit/engine
|
|
2
|
+
|
|
3
|
+
Runtime engine for Rigkit workflows.
|
|
4
|
+
|
|
5
|
+
This package loads `rig.config.ts`, evaluates workflow nodes, manages graph-based `.rigkit/state.sqlite` node-run cache, runs workspace lifecycle hooks, talks to registered workflow providers, presents provider-owned interaction URLs, and exposes APIs used by the CLI and future app.
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rigkit/engine",
|
|
3
|
+
"version": "0.1.8",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/freestyle-sh/rigkit.git",
|
|
8
|
+
"directory": "packages/engine"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/index.ts",
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"drizzle-kit": "^0.31.10",
|
|
20
|
+
"drizzle-orm": "^0.45.2"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/bun": "latest",
|
|
24
|
+
"typescript": "latest"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsc --noEmit",
|
|
31
|
+
"typecheck": "tsc --noEmit",
|
|
32
|
+
"test": "bun test"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/authoring.ts
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
EnvResolver,
|
|
3
|
+
RigkitConfigDefinition,
|
|
4
|
+
JsonObject,
|
|
5
|
+
OutputSchema,
|
|
6
|
+
OutputSchemaValue,
|
|
7
|
+
WorkflowDefinition,
|
|
8
|
+
WorkflowCreateDefinition,
|
|
9
|
+
WorkflowCreateHandler,
|
|
10
|
+
WorkflowInputFieldDefinition,
|
|
11
|
+
WorkflowInputShape,
|
|
12
|
+
WorkflowOperationDefinition,
|
|
13
|
+
WorkflowOperationInputBuilder,
|
|
14
|
+
WorkflowOperationInputHelpers,
|
|
15
|
+
WorkflowOperationOptions,
|
|
16
|
+
WorkflowNodeDefinition,
|
|
17
|
+
WorkflowProviderDefinition,
|
|
18
|
+
WorkflowProviderMap,
|
|
19
|
+
WorkflowSequenceBuilder,
|
|
20
|
+
WorkflowTaskHandler,
|
|
21
|
+
WorkflowTaskNode,
|
|
22
|
+
WorkflowTaskOptions,
|
|
23
|
+
WorkflowTaskResult,
|
|
24
|
+
WorkflowWorkspaceDefinition,
|
|
25
|
+
} from "./types.ts";
|
|
26
|
+
|
|
27
|
+
const reservedTaskContextKeys = new Set(["ctx", "runtime", "providers"]);
|
|
28
|
+
const reservedHostOperationIds = new Set(["init", "doctor", "projects", "run", "ls", "help", "version", "completion"]);
|
|
29
|
+
|
|
30
|
+
const readEnv = (name: string, fallback?: string): string => {
|
|
31
|
+
const value = process.env[name];
|
|
32
|
+
if (value !== undefined && value !== "") return value;
|
|
33
|
+
if (fallback !== undefined) return fallback;
|
|
34
|
+
throw new Error(`Missing required environment variable ${name}`);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const env: EnvResolver = Object.assign(readEnv, {
|
|
38
|
+
secret: readEnv,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export function workflow<const Name extends string, const Providers extends WorkflowProviderMap>(
|
|
42
|
+
name: Name,
|
|
43
|
+
options: { providers: Providers },
|
|
44
|
+
): WorkflowDefinition<Name, Providers> {
|
|
45
|
+
validateProviders(options.providers);
|
|
46
|
+
|
|
47
|
+
const app = {
|
|
48
|
+
kind: "rigkit.workflow" as const,
|
|
49
|
+
name,
|
|
50
|
+
providers: options.providers,
|
|
51
|
+
sequence: <InputContext extends JsonObject = {}>(sequenceName: string) =>
|
|
52
|
+
createSequence(app as unknown as WorkflowDefinition<string, Providers>, sequenceName, []),
|
|
53
|
+
task: (taskName: string, optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, {}, any>, maybeHandler?: WorkflowTaskHandler<Providers, {}, any>) =>
|
|
54
|
+
createTask(app as unknown as WorkflowDefinition<string, Providers>, taskName, optionsOrHandler as any, maybeHandler as any),
|
|
55
|
+
step: (taskName: string, optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, {}, any>, maybeHandler?: WorkflowTaskHandler<Providers, {}, any>) =>
|
|
56
|
+
createTask(app as unknown as WorkflowDefinition<string, Providers>, taskName, optionsOrHandler as any, maybeHandler as any),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return app as unknown as WorkflowDefinition<Name, Providers>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function sequence<
|
|
63
|
+
Providers extends WorkflowProviderMap = WorkflowProviderMap,
|
|
64
|
+
InputContext extends JsonObject = {},
|
|
65
|
+
>(name: string): WorkflowSequenceBuilder<Providers, InputContext, InputContext> {
|
|
66
|
+
return workflow(name, { providers: {} as Providers }).sequence<InputContext>(name);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function defineConfig<
|
|
70
|
+
const Providers extends WorkflowProviderMap,
|
|
71
|
+
const Workflows extends Record<string, WorkflowNodeDefinition<any, any, any>>,
|
|
72
|
+
>(options: {
|
|
73
|
+
providers: Providers;
|
|
74
|
+
workflows: Workflows;
|
|
75
|
+
}): RigkitConfigDefinition<Providers, Workflows> {
|
|
76
|
+
validateProviders(options.providers);
|
|
77
|
+
for (const [name, node] of Object.entries(options.workflows)) {
|
|
78
|
+
if (!isWorkflowNode(node)) {
|
|
79
|
+
throw new Error(`Workflow ${name} is not a valid Rigkit workflow node`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
kind: "rigkit.config",
|
|
84
|
+
providers: options.providers,
|
|
85
|
+
workflows: options.workflows,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function defineProvider<
|
|
90
|
+
const ProviderId extends string,
|
|
91
|
+
const Config extends object,
|
|
92
|
+
Runtime = unknown,
|
|
93
|
+
WorkspaceContext extends object = object,
|
|
94
|
+
>(
|
|
95
|
+
providerId: ProviderId,
|
|
96
|
+
config: WorkflowProviderDefinition<ProviderId, Config, Runtime, WorkspaceContext>["config"],
|
|
97
|
+
plugin?: unknown,
|
|
98
|
+
): WorkflowProviderDefinition<ProviderId, Config, Runtime, WorkspaceContext> {
|
|
99
|
+
return {
|
|
100
|
+
kind: "rigkit.provider",
|
|
101
|
+
providerId,
|
|
102
|
+
config,
|
|
103
|
+
plugin,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function isWorkflow(value: unknown): value is WorkflowDefinition {
|
|
108
|
+
return Boolean(value && typeof value === "object" && getKind(value) === "rigkit.workflow");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function isWorkflowNode(value: unknown): value is WorkflowNodeDefinition<any, any, any> {
|
|
112
|
+
return Boolean(value && typeof value === "object" && getKind(value) === "rigkit.workflow-node");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function isRigkitConfig(value: unknown): value is RigkitConfigDefinition {
|
|
116
|
+
return Boolean(value && typeof value === "object" && getKind(value) === "rigkit.config");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function isProviderDefinition(value: unknown): value is WorkflowProviderDefinition {
|
|
120
|
+
return Boolean(value && typeof value === "object" && getKind(value) === "rigkit.provider");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function createSequence<Providers extends WorkflowProviderMap, InputContext extends JsonObject, OutputContext extends JsonObject>(
|
|
124
|
+
app: WorkflowDefinition<string, Providers>,
|
|
125
|
+
name: string,
|
|
126
|
+
children: readonly WorkflowNodeDefinition<Providers, any, any>[],
|
|
127
|
+
workspace?: WorkflowWorkspaceDefinition<Providers, OutputContext>,
|
|
128
|
+
create?: WorkflowCreateDefinition<Providers, OutputContext>,
|
|
129
|
+
operations: readonly WorkflowOperationDefinition<Providers, any>[] = [],
|
|
130
|
+
): WorkflowSequenceBuilder<Providers, InputContext, OutputContext> {
|
|
131
|
+
const node = {
|
|
132
|
+
kind: "rigkit.workflow-node" as const,
|
|
133
|
+
nodeKind: "sequence" as const,
|
|
134
|
+
name,
|
|
135
|
+
workflow: app,
|
|
136
|
+
children,
|
|
137
|
+
workspaceDefinition: workspace,
|
|
138
|
+
createDefinition: create,
|
|
139
|
+
operations,
|
|
140
|
+
task: (
|
|
141
|
+
taskName: string,
|
|
142
|
+
optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, OutputContext, any>,
|
|
143
|
+
maybeHandler?: WorkflowTaskHandler<Providers, OutputContext, any>,
|
|
144
|
+
) => {
|
|
145
|
+
const task = createTask(app, taskName, optionsOrHandler as any, maybeHandler as any);
|
|
146
|
+
return createSequence(app, name, [...children, task], workspace, create, operations);
|
|
147
|
+
},
|
|
148
|
+
step: (
|
|
149
|
+
taskName: string,
|
|
150
|
+
optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, OutputContext, any>,
|
|
151
|
+
maybeHandler?: WorkflowTaskHandler<Providers, OutputContext, any>,
|
|
152
|
+
) => {
|
|
153
|
+
const task = createTask(app, taskName, optionsOrHandler as any, maybeHandler as any);
|
|
154
|
+
return createSequence(app, name, [...children, task], workspace, create, operations);
|
|
155
|
+
},
|
|
156
|
+
add: (child: WorkflowNodeDefinition<Providers, any, any>) => {
|
|
157
|
+
assertSameWorkflow(app, child);
|
|
158
|
+
return createSequence(app, name, [...children, child], workspace, create, operations);
|
|
159
|
+
},
|
|
160
|
+
parallel: (branches: Record<string, WorkflowNodeDefinition<Providers, any, any>>) => {
|
|
161
|
+
for (const [branchName, branch] of Object.entries(branches)) {
|
|
162
|
+
assertSameWorkflow(app, branch);
|
|
163
|
+
if (!branchName) throw new Error(`Parallel branch names must be non-empty`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const parallelNode: WorkflowNodeDefinition<Providers, OutputContext, any> & {
|
|
167
|
+
nodeKind: "parallel";
|
|
168
|
+
branches: Record<string, WorkflowNodeDefinition<Providers, any, any>>;
|
|
169
|
+
} = {
|
|
170
|
+
kind: "rigkit.workflow-node",
|
|
171
|
+
nodeKind: "parallel",
|
|
172
|
+
name: "parallel",
|
|
173
|
+
workflow: app,
|
|
174
|
+
branches,
|
|
175
|
+
};
|
|
176
|
+
return createSequence(app, name, [...children, parallelNode], workspace, create, operations);
|
|
177
|
+
},
|
|
178
|
+
workspace: (definition: WorkflowWorkspaceDefinition<Providers, OutputContext>) =>
|
|
179
|
+
createSequence(app, name, children, definition, create, operations),
|
|
180
|
+
create: (handler: WorkflowCreateHandler<Providers, OutputContext, any>) =>
|
|
181
|
+
createSequence(app, name, children, workspace, { handler }, operations),
|
|
182
|
+
operation: (id: string, options: WorkflowOperationOptions<Providers, any>) =>
|
|
183
|
+
createSequence(app, name, children, workspace, create, [...operations, createOperation(id, options)]),
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
return node as unknown as WorkflowSequenceBuilder<Providers, InputContext, OutputContext>;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function createOperation<Providers extends WorkflowProviderMap, Input extends object>(
|
|
190
|
+
id: string,
|
|
191
|
+
options: WorkflowOperationOptions<Providers, Input>,
|
|
192
|
+
): WorkflowOperationDefinition<Providers, Input> {
|
|
193
|
+
const normalized = id.trim();
|
|
194
|
+
if (!normalized) throw new Error(`Operation ids must be non-empty`);
|
|
195
|
+
if (reservedHostOperationIds.has(normalized)) {
|
|
196
|
+
throw new Error(`Operation id ${normalized} is reserved by the Rigkit host`);
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
id: normalized,
|
|
200
|
+
title: options.title,
|
|
201
|
+
description: options.description,
|
|
202
|
+
createsWorkspace: options.createsWorkspace,
|
|
203
|
+
requiredHostMethods: normalizeHostMethodRequirements(options.requiredHostMethods),
|
|
204
|
+
requiredHostCapabilities: normalizeHostCapabilityRequirements(options.requiredHostCapabilities),
|
|
205
|
+
input: typeof options.input === "function"
|
|
206
|
+
? options.input(createOperationInputHelpers())
|
|
207
|
+
: options.input,
|
|
208
|
+
run: options.run,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function normalizeHostMethodRequirements(
|
|
213
|
+
methods: WorkflowOperationOptions<any, any>["requiredHostMethods"],
|
|
214
|
+
) {
|
|
215
|
+
return methods?.map((method) => {
|
|
216
|
+
const id = method.id.trim();
|
|
217
|
+
if (!id) throw new Error(`Host method requirements must have non-empty ids`);
|
|
218
|
+
return {
|
|
219
|
+
id,
|
|
220
|
+
...(method.modes?.length ? { modes: [...method.modes] } : {}),
|
|
221
|
+
};
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function normalizeHostCapabilityRequirements(
|
|
226
|
+
capabilities: WorkflowOperationOptions<any, any>["requiredHostCapabilities"],
|
|
227
|
+
) {
|
|
228
|
+
return capabilities?.map((capability) => {
|
|
229
|
+
const id = capability.id.trim();
|
|
230
|
+
if (!id) throw new Error(`Host capability requirements must have non-empty ids`);
|
|
231
|
+
const schemaHash = capability.schemaHash?.trim();
|
|
232
|
+
return {
|
|
233
|
+
id,
|
|
234
|
+
...(schemaHash ? { schemaHash } : {}),
|
|
235
|
+
};
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function createOperationInputHelpers(): WorkflowOperationInputHelpers {
|
|
240
|
+
return {
|
|
241
|
+
workspaceInput: (options) => createOperationInputBuilder([{
|
|
242
|
+
kind: "workspace",
|
|
243
|
+
name: options.name,
|
|
244
|
+
description: options.description,
|
|
245
|
+
position: options.position,
|
|
246
|
+
required: options.required ?? true,
|
|
247
|
+
}]),
|
|
248
|
+
string: (options) => ({
|
|
249
|
+
kind: "string",
|
|
250
|
+
name: options.name ?? "",
|
|
251
|
+
description: options.description,
|
|
252
|
+
position: options.position,
|
|
253
|
+
required: options.required ?? options.defaultValue === undefined,
|
|
254
|
+
defaultValue: options.defaultValue,
|
|
255
|
+
}),
|
|
256
|
+
boolean: (options) => ({
|
|
257
|
+
kind: "boolean",
|
|
258
|
+
name: options.name ?? "",
|
|
259
|
+
description: options.description,
|
|
260
|
+
position: options.position,
|
|
261
|
+
required: options.required ?? false,
|
|
262
|
+
defaultValue: options.defaultValue,
|
|
263
|
+
}),
|
|
264
|
+
number: (options) => ({
|
|
265
|
+
kind: "number",
|
|
266
|
+
name: options.name ?? "",
|
|
267
|
+
description: options.description,
|
|
268
|
+
position: options.position,
|
|
269
|
+
required: options.required ?? options.defaultValue === undefined,
|
|
270
|
+
defaultValue: options.defaultValue,
|
|
271
|
+
}),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function createOperationInputBuilder<Input extends object>(
|
|
276
|
+
fields: readonly WorkflowInputFieldDefinition[],
|
|
277
|
+
): WorkflowOperationInputBuilder<Input> {
|
|
278
|
+
return {
|
|
279
|
+
fields,
|
|
280
|
+
extend(shape: WorkflowInputShape) {
|
|
281
|
+
const nextFields = Object.entries(shape).map(([name, field]) => ({
|
|
282
|
+
...field,
|
|
283
|
+
name: field.name || name,
|
|
284
|
+
}));
|
|
285
|
+
return createOperationInputBuilder([...fields, ...nextFields]);
|
|
286
|
+
},
|
|
287
|
+
} as WorkflowOperationInputBuilder<Input>;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function createTask<Providers extends WorkflowProviderMap, InputContext extends JsonObject>(
|
|
291
|
+
app: WorkflowDefinition<string, Providers>,
|
|
292
|
+
name: string,
|
|
293
|
+
optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, InputContext, any>,
|
|
294
|
+
maybeHandler?: WorkflowTaskHandler<Providers, InputContext, any>,
|
|
295
|
+
): WorkflowTaskNode<Providers, InputContext, any> {
|
|
296
|
+
const options = typeof optionsOrHandler === "function" ? undefined : optionsOrHandler;
|
|
297
|
+
const handler = (typeof optionsOrHandler === "function" ? optionsOrHandler : maybeHandler) as
|
|
298
|
+
| WorkflowTaskHandler<Providers, InputContext, any>
|
|
299
|
+
| undefined;
|
|
300
|
+
if (!handler) throw new Error(`Task ${name} is missing a handler`);
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
kind: "rigkit.workflow-node",
|
|
304
|
+
nodeKind: "task",
|
|
305
|
+
name,
|
|
306
|
+
workflow: app,
|
|
307
|
+
options,
|
|
308
|
+
handler,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function validateProviders(providers: WorkflowProviderMap): void {
|
|
313
|
+
for (const [name, provider] of Object.entries(providers)) {
|
|
314
|
+
if (reservedTaskContextKeys.has(name)) {
|
|
315
|
+
throw new Error(`Provider name ${name} is reserved by the task context`);
|
|
316
|
+
}
|
|
317
|
+
if (!isProviderDefinition(provider)) {
|
|
318
|
+
throw new Error(`Provider ${name} is not a valid Rigkit provider`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function assertSameWorkflow(
|
|
324
|
+
app: WorkflowDefinition<string, any>,
|
|
325
|
+
node: WorkflowNodeDefinition<any, any, any>,
|
|
326
|
+
): void {
|
|
327
|
+
if (node.workflow !== app) {
|
|
328
|
+
throw new Error(`Node ${node.name} belongs to a different workflow`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function getKind(value: object): unknown {
|
|
333
|
+
return (value as { kind?: unknown }).kind;
|
|
334
|
+
}
|
package/src/db/index.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { Database } from "bun:sqlite";
|
|
4
|
+
import { drizzle, type BunSQLiteDatabase } from "drizzle-orm/bun-sqlite";
|
|
5
|
+
import { coreSchema, type CoreSchema } from "./schema/index.ts";
|
|
6
|
+
|
|
7
|
+
export const RIGKIT_STATE_SCHEMA_VERSION = "drizzle-push";
|
|
8
|
+
|
|
9
|
+
export type RigkitDatabase<TSchema extends Record<string, unknown> = CoreSchema> =
|
|
10
|
+
BunSQLiteDatabase<TSchema> & { $client: Database };
|
|
11
|
+
|
|
12
|
+
export type RigkitDatabaseSchema = Record<string, unknown>;
|
|
13
|
+
|
|
14
|
+
export type CreateRigkitDatabaseOptions<TSchema extends RigkitDatabaseSchema = CoreSchema> = {
|
|
15
|
+
schema?: TSchema;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type SchemaSyncResult = {
|
|
19
|
+
applied: string[];
|
|
20
|
+
schemaVersion: string;
|
|
21
|
+
statements: string[];
|
|
22
|
+
warnings: string[];
|
|
23
|
+
hasDataLoss: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type DrizzleKitBunSQLiteDatabase = Pick<RigkitDatabase<RigkitDatabaseSchema>, "all" | "run">;
|
|
27
|
+
|
|
28
|
+
type PushSQLiteSchemaResult = {
|
|
29
|
+
hasDataLoss: boolean;
|
|
30
|
+
warnings: string[];
|
|
31
|
+
statementsToExecute: string[];
|
|
32
|
+
apply(): Promise<void>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type PushSQLiteSchemaForBun = (
|
|
36
|
+
imports: RigkitDatabaseSchema,
|
|
37
|
+
drizzleInstance: DrizzleKitBunSQLiteDatabase,
|
|
38
|
+
) => Promise<PushSQLiteSchemaResult>;
|
|
39
|
+
|
|
40
|
+
export function createRigkitDatabase<TSchema extends RigkitDatabaseSchema = CoreSchema>(
|
|
41
|
+
path: string,
|
|
42
|
+
options: CreateRigkitDatabaseOptions<TSchema> = {},
|
|
43
|
+
): RigkitDatabase<TSchema> {
|
|
44
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
45
|
+
const db = drizzle(new Database(path, { create: true }), {
|
|
46
|
+
schema: options.schema ?? (coreSchema as unknown as TSchema),
|
|
47
|
+
});
|
|
48
|
+
db.run("PRAGMA journal_mode = WAL");
|
|
49
|
+
db.run("PRAGMA foreign_keys = ON");
|
|
50
|
+
return db;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function syncRigkitDatabaseSchema<TSchema extends RigkitDatabaseSchema>(
|
|
54
|
+
db: RigkitDatabase<TSchema>,
|
|
55
|
+
schema: TSchema,
|
|
56
|
+
): Promise<SchemaSyncResult> {
|
|
57
|
+
try {
|
|
58
|
+
const result = await pushRigkitDatabaseSchema(db, schema);
|
|
59
|
+
return toSchemaSyncResult(result);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
const resetStatements = resetRigkitDatabase(db);
|
|
62
|
+
const result = await pushRigkitDatabaseSchema(db, schema);
|
|
63
|
+
return toSchemaSyncResult(result, {
|
|
64
|
+
resetStatements,
|
|
65
|
+
resetReason: errorMessage(error),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function pushRigkitDatabaseSchema<TSchema extends RigkitDatabaseSchema>(
|
|
71
|
+
db: RigkitDatabase<TSchema>,
|
|
72
|
+
schema: TSchema,
|
|
73
|
+
): Promise<PushSQLiteSchemaResult> {
|
|
74
|
+
const drizzleKitApi = ["drizzle-kit", "api"].join("/");
|
|
75
|
+
const { pushSQLiteSchema } = await import(drizzleKitApi);
|
|
76
|
+
const pushSchema = pushSQLiteSchema as unknown as PushSQLiteSchemaForBun;
|
|
77
|
+
const result = await silenceStdout(() => pushSchema(schema, db));
|
|
78
|
+
await silenceStdout(() => result.apply());
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function toSchemaSyncResult(
|
|
83
|
+
result: PushSQLiteSchemaResult,
|
|
84
|
+
reset?: { resetStatements: string[]; resetReason: string },
|
|
85
|
+
): SchemaSyncResult {
|
|
86
|
+
const statements = [...(reset?.resetStatements ?? []), ...result.statementsToExecute];
|
|
87
|
+
const warnings = [...result.warnings];
|
|
88
|
+
if (reset) {
|
|
89
|
+
warnings.unshift(`Reset Rigkit state database after Drizzle push failed: ${reset.resetReason}`);
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
applied: statements.length > 0 ? [RIGKIT_STATE_SCHEMA_VERSION] : [],
|
|
93
|
+
schemaVersion: RIGKIT_STATE_SCHEMA_VERSION,
|
|
94
|
+
statements,
|
|
95
|
+
warnings,
|
|
96
|
+
hasDataLoss: reset !== undefined || result.hasDataLoss,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function resetRigkitDatabase<TSchema extends RigkitDatabaseSchema>(db: RigkitDatabase<TSchema>): string[] {
|
|
101
|
+
const rows = db.$client
|
|
102
|
+
.query(`
|
|
103
|
+
SELECT type, name
|
|
104
|
+
FROM sqlite_schema
|
|
105
|
+
WHERE type IN ('table', 'view', 'trigger')
|
|
106
|
+
AND name NOT LIKE 'sqlite_%'
|
|
107
|
+
ORDER BY CASE type
|
|
108
|
+
WHEN 'view' THEN 0
|
|
109
|
+
WHEN 'trigger' THEN 1
|
|
110
|
+
ELSE 2
|
|
111
|
+
END
|
|
112
|
+
`)
|
|
113
|
+
.all() as Array<{ type: "table" | "view" | "trigger"; name: string }>;
|
|
114
|
+
|
|
115
|
+
const statements = [
|
|
116
|
+
"PRAGMA foreign_keys=OFF",
|
|
117
|
+
...rows.map((row) => `DROP ${row.type.toUpperCase()} IF EXISTS ${quoteSqlIdentifier(row.name)}`),
|
|
118
|
+
"PRAGMA foreign_keys=ON",
|
|
119
|
+
];
|
|
120
|
+
const dropStatements = statements.slice(1, -1);
|
|
121
|
+
const reset = db.$client.transaction(() => {
|
|
122
|
+
for (const sql of dropStatements) {
|
|
123
|
+
db.$client.run(sql);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
db.$client.run("PRAGMA foreign_keys=OFF");
|
|
127
|
+
try {
|
|
128
|
+
reset();
|
|
129
|
+
} finally {
|
|
130
|
+
db.$client.run("PRAGMA foreign_keys=ON");
|
|
131
|
+
}
|
|
132
|
+
return statements;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function quoteSqlIdentifier(name: string): string {
|
|
136
|
+
return `"${name.replaceAll('"', '""')}"`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function silenceStdout<T>(run: () => Promise<T>): Promise<T> {
|
|
140
|
+
const write = process.stdout.write;
|
|
141
|
+
process.stdout.write = (() => true) as typeof process.stdout.write;
|
|
142
|
+
try {
|
|
143
|
+
return await run();
|
|
144
|
+
} finally {
|
|
145
|
+
process.stdout.write = write;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function errorMessage(error: unknown): string {
|
|
150
|
+
if (error instanceof Error) return error.message;
|
|
151
|
+
if (typeof error === "string") return error;
|
|
152
|
+
try {
|
|
153
|
+
return JSON.stringify(error);
|
|
154
|
+
} catch {
|
|
155
|
+
return String(error);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
|
|
2
|
+
import type { JsonValue } from "../../types.ts";
|
|
3
|
+
|
|
4
|
+
export const workspaces = sqliteTable(
|
|
5
|
+
"workspaces",
|
|
6
|
+
{
|
|
7
|
+
id: text("id").primaryKey(),
|
|
8
|
+
name: text("name").notNull(),
|
|
9
|
+
providerId: text("provider_id").notNull(),
|
|
10
|
+
workflow: text("workflow").notNull(),
|
|
11
|
+
resourceId: text("resource_id").notNull(),
|
|
12
|
+
snapshotId: text("snapshot_id"),
|
|
13
|
+
sourceRef: text("source_ref_json", { mode: "json" }).$type<JsonValue>().notNull(),
|
|
14
|
+
context: text("context_json", { mode: "json" }).$type<Record<string, JsonValue>>().notNull(),
|
|
15
|
+
createdAt: text("created_at").notNull(),
|
|
16
|
+
updatedAt: text("updated_at").notNull(),
|
|
17
|
+
metadata: text("metadata_json", { mode: "json" }).$type<Record<string, JsonValue>>().notNull(),
|
|
18
|
+
},
|
|
19
|
+
(table) => [
|
|
20
|
+
uniqueIndex("workspaces_name_idx").on(table.name),
|
|
21
|
+
index("workspaces_provider_resource_idx").on(table.providerId, table.resourceId),
|
|
22
|
+
],
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
export const workflowNodeRuns = sqliteTable(
|
|
26
|
+
"workflow_node_runs",
|
|
27
|
+
{
|
|
28
|
+
id: text("id").primaryKey(),
|
|
29
|
+
workflow: text("workflow").notNull(),
|
|
30
|
+
nodePath: text("node_path").notNull(),
|
|
31
|
+
nodeName: text("node_name").notNull(),
|
|
32
|
+
nodeKind: text("node_kind").notNull(),
|
|
33
|
+
nodeKey: text("node_key").notNull(),
|
|
34
|
+
providerFingerprint: text("provider_fingerprint").notNull(),
|
|
35
|
+
upstreamRunIds: text("upstream_run_ids_json", { mode: "json" }).$type<string[]>().notNull(),
|
|
36
|
+
output: text("output_json", { mode: "json" }).$type<Record<string, JsonValue>>().notNull(),
|
|
37
|
+
artifacts: text("artifacts_json", { mode: "json" }).$type<JsonValue[]>().notNull(),
|
|
38
|
+
invalidated: integer("invalidated", { mode: "boolean" }).notNull().default(false),
|
|
39
|
+
createdAt: text("created_at").notNull(),
|
|
40
|
+
metadata: text("metadata_json", { mode: "json" }).$type<Record<string, JsonValue>>().notNull(),
|
|
41
|
+
},
|
|
42
|
+
(table) => [
|
|
43
|
+
index("workflow_node_runs_lookup_idx").on(
|
|
44
|
+
table.workflow,
|
|
45
|
+
table.nodePath,
|
|
46
|
+
table.nodeKey,
|
|
47
|
+
table.providerFingerprint,
|
|
48
|
+
),
|
|
49
|
+
index("workflow_node_runs_created_idx").on(table.createdAt),
|
|
50
|
+
],
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
export const providerState = sqliteTable(
|
|
54
|
+
"provider_state",
|
|
55
|
+
{
|
|
56
|
+
providerId: text("provider_id").notNull(),
|
|
57
|
+
key: text("key").notNull(),
|
|
58
|
+
value: text("value_json", { mode: "json" }).$type<JsonValue>().notNull(),
|
|
59
|
+
createdAt: text("created_at").notNull(),
|
|
60
|
+
updatedAt: text("updated_at").notNull(),
|
|
61
|
+
},
|
|
62
|
+
(table) => [
|
|
63
|
+
uniqueIndex("provider_state_provider_key_idx").on(table.providerId, table.key),
|
|
64
|
+
index("provider_state_provider_idx").on(table.providerId),
|
|
65
|
+
],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
export const runtimeMetadata = sqliteTable(
|
|
69
|
+
"runtime_metadata",
|
|
70
|
+
{
|
|
71
|
+
key: text("key").primaryKey(),
|
|
72
|
+
value: text("value_json", { mode: "json" }).$type<JsonValue>().notNull(),
|
|
73
|
+
updatedAt: text("updated_at").notNull(),
|
|
74
|
+
},
|
|
75
|
+
);
|