@kahitsan/plugin-sdk 0.1.0-staging.2 → 0.1.0-staging.20
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/flow-bundle.d.ts +2 -0
- package/dist/flow-bundle.js +195 -0
- package/dist/index.d.ts +90 -10
- package/dist/index.js +2 -18
- package/package.json +7 -3
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// src/flow/flow-spec.ts
|
|
2
|
+
function edge(to, label) {
|
|
3
|
+
return label === void 0 ? { id: "out", to } : { id: label, to, label };
|
|
4
|
+
}
|
|
5
|
+
function node(id, kind, label, opts) {
|
|
6
|
+
return { id, kind, label, detail: opts?.detail, out: opts?.out };
|
|
7
|
+
}
|
|
8
|
+
function defineFlow(def) {
|
|
9
|
+
const ids = new Set(def.nodes.map((n) => n.id));
|
|
10
|
+
for (const n of def.nodes) {
|
|
11
|
+
for (const p of n.out ?? []) {
|
|
12
|
+
if (!ids.has(p.to)) {
|
|
13
|
+
throw new Error(`flow "${def.id}": node "${n.id}" connects to unknown node "${p.to}"`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return def;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// src/flow/flow-builder.ts
|
|
21
|
+
var FlowSteps = class {
|
|
22
|
+
constructor(prefix) {
|
|
23
|
+
this.prefix = prefix;
|
|
24
|
+
}
|
|
25
|
+
prefix;
|
|
26
|
+
nodes = [];
|
|
27
|
+
make(kind, label, detail) {
|
|
28
|
+
const def = {
|
|
29
|
+
id: `${this.prefix}_${kind}_${this.nodes.length}`,
|
|
30
|
+
kind,
|
|
31
|
+
label,
|
|
32
|
+
...detail ? { detail } : {}
|
|
33
|
+
};
|
|
34
|
+
this.nodes.push(def);
|
|
35
|
+
const handle = {
|
|
36
|
+
id: def.id,
|
|
37
|
+
to(target, label2) {
|
|
38
|
+
def.out = def.out ?? [];
|
|
39
|
+
def.out.push(label2 ? { id: label2, to: target.id, label: label2 } : { id: "out", to: target.id });
|
|
40
|
+
return target;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
return handle;
|
|
44
|
+
}
|
|
45
|
+
/** A UI event that starts/continues the flow (a button, a selection). */
|
|
46
|
+
trigger(label) {
|
|
47
|
+
return this.make("trigger", label);
|
|
48
|
+
}
|
|
49
|
+
/** A data source / list the screen shows. */
|
|
50
|
+
data(label, detail) {
|
|
51
|
+
return this.make("data", label, detail);
|
|
52
|
+
}
|
|
53
|
+
/** A fetch/load into the current screen. */
|
|
54
|
+
load(label, detail) {
|
|
55
|
+
return this.make("load", label, detail);
|
|
56
|
+
}
|
|
57
|
+
/** Opens an overlay / form. */
|
|
58
|
+
modal(label) {
|
|
59
|
+
return this.make("modal", label);
|
|
60
|
+
}
|
|
61
|
+
/** A call out to another service/capability; `target` is its identifier. */
|
|
62
|
+
call(target, label) {
|
|
63
|
+
return this.make("call", label ?? target, target);
|
|
64
|
+
}
|
|
65
|
+
/** A pure computation (apply a discount, total a cart). */
|
|
66
|
+
compute(label) {
|
|
67
|
+
return this.make("compute", label);
|
|
68
|
+
}
|
|
69
|
+
/** A branch — wire its outcomes with `.to(target, "yes")` / `.to(target, "no")`. */
|
|
70
|
+
condition(label) {
|
|
71
|
+
return this.make("condition", label);
|
|
72
|
+
}
|
|
73
|
+
/** A write / command. */
|
|
74
|
+
commit(label, detail) {
|
|
75
|
+
return this.make("commit", label, detail);
|
|
76
|
+
}
|
|
77
|
+
/** Emits a domain event. */
|
|
78
|
+
emit(event) {
|
|
79
|
+
return this.make("emit", event);
|
|
80
|
+
}
|
|
81
|
+
/** A UI effect — refresh / toast / navigate / close. */
|
|
82
|
+
effect(label) {
|
|
83
|
+
return this.make("effect", label);
|
|
84
|
+
}
|
|
85
|
+
/** An end state. */
|
|
86
|
+
terminal(label) {
|
|
87
|
+
return this.make("terminal", label);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
function buildFlow(id, title, build) {
|
|
91
|
+
const f = new FlowSteps(id.replace(/[^a-zA-Z0-9]+/g, "_"));
|
|
92
|
+
build(f);
|
|
93
|
+
return defineFlow({ id, title, nodes: f.nodes });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/flow/flow-runtime.ts
|
|
97
|
+
function methodFor(kind, explicit) {
|
|
98
|
+
if (explicit) return explicit;
|
|
99
|
+
return kind === "load" ? "GET" : "POST";
|
|
100
|
+
}
|
|
101
|
+
function branchLabel(result) {
|
|
102
|
+
if (typeof result === "string") return result;
|
|
103
|
+
return result ? "yes" : "no";
|
|
104
|
+
}
|
|
105
|
+
async function runRequest(node2, ctx) {
|
|
106
|
+
if (!node2.request) return;
|
|
107
|
+
const spec = node2.request(ctx);
|
|
108
|
+
const init = { method: methodFor(node2.kind, spec.method) };
|
|
109
|
+
if (spec.body !== void 0) {
|
|
110
|
+
init.body = JSON.stringify(spec.body);
|
|
111
|
+
init.headers = { "Content-Type": "application/json" };
|
|
112
|
+
}
|
|
113
|
+
const res = await ctx.fetch(spec.url, init);
|
|
114
|
+
if (res.ok) {
|
|
115
|
+
ctx.state[node2.id] = await res.json().catch(() => null);
|
|
116
|
+
} else {
|
|
117
|
+
ctx.state[node2.id] = null;
|
|
118
|
+
ctx.state.__error = await res.json().catch(() => ({ error: res.statusText }));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function errorMessage(ctx) {
|
|
122
|
+
const err = ctx.state.__error;
|
|
123
|
+
return err?.error ?? err?.message;
|
|
124
|
+
}
|
|
125
|
+
function runEffect(node2, ctx) {
|
|
126
|
+
const ui = ctx.ui;
|
|
127
|
+
if (node2.effect === "refresh") ui?.refresh?.();
|
|
128
|
+
else if (node2.effect === "toast") ui?.toast?.(errorMessage(ctx) ?? node2.arg ?? node2.label);
|
|
129
|
+
else if (node2.effect === "close") ui?.close?.();
|
|
130
|
+
else if (node2.effect === "navigate") ui?.navigate?.(node2.arg ?? "/");
|
|
131
|
+
}
|
|
132
|
+
async function runModal(node2, ctx) {
|
|
133
|
+
if (!ctx.ui?.openModal) return {};
|
|
134
|
+
const input = await ctx.ui.openModal(node2);
|
|
135
|
+
if (input === null) return { halt: true };
|
|
136
|
+
Object.assign(ctx.state, input);
|
|
137
|
+
return {};
|
|
138
|
+
}
|
|
139
|
+
async function executeNode(node2, ctx) {
|
|
140
|
+
switch (node2.kind) {
|
|
141
|
+
case "load":
|
|
142
|
+
case "commit":
|
|
143
|
+
await runRequest(node2, ctx);
|
|
144
|
+
return {};
|
|
145
|
+
case "call":
|
|
146
|
+
if (node2.invoke && ctx.call) {
|
|
147
|
+
ctx.state[node2.id] = await ctx.call(node2.invoke.target, node2.invoke.args?.(ctx));
|
|
148
|
+
}
|
|
149
|
+
return {};
|
|
150
|
+
case "compute":
|
|
151
|
+
if (node2.compute) ctx.state[node2.id] = node2.compute(ctx);
|
|
152
|
+
return {};
|
|
153
|
+
case "condition":
|
|
154
|
+
return { branch: branchLabel(node2.when ? node2.when(ctx) : true) };
|
|
155
|
+
case "modal":
|
|
156
|
+
return runModal(node2, ctx);
|
|
157
|
+
case "effect":
|
|
158
|
+
runEffect(node2, ctx);
|
|
159
|
+
return {};
|
|
160
|
+
case "terminal":
|
|
161
|
+
return { halt: true };
|
|
162
|
+
default:
|
|
163
|
+
return {};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function pickNext(node2, branch) {
|
|
167
|
+
const outs = node2.out ?? [];
|
|
168
|
+
if (branch) return outs.find((o) => o.label === branch) ?? outs[0];
|
|
169
|
+
return outs[0];
|
|
170
|
+
}
|
|
171
|
+
async function runFlow(flow, startId, ctx) {
|
|
172
|
+
const byId = new Map(flow.nodes.map((n) => [n.id, n]));
|
|
173
|
+
const walked = /* @__PURE__ */ new Set();
|
|
174
|
+
let current = byId.get(startId);
|
|
175
|
+
while (current) {
|
|
176
|
+
const { branch, halt } = await executeNode(current, ctx);
|
|
177
|
+
if (halt) return;
|
|
178
|
+
const next = pickNext(current, branch);
|
|
179
|
+
if (!next) return;
|
|
180
|
+
const edgeKey = `${current.id}->${next.to}`;
|
|
181
|
+
if (walked.has(edgeKey)) return;
|
|
182
|
+
walked.add(edgeKey);
|
|
183
|
+
const target = byId.get(next.to);
|
|
184
|
+
if (target?.kind === "trigger") return;
|
|
185
|
+
current = target;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
export {
|
|
189
|
+
FlowSteps,
|
|
190
|
+
buildFlow,
|
|
191
|
+
defineFlow,
|
|
192
|
+
edge,
|
|
193
|
+
node,
|
|
194
|
+
runFlow
|
|
195
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Router, Request, RequestHandler, Response, NextFunction, Express } from 'express';
|
|
1
|
+
import { Router, Request, RequestHandler, Response as Response$1, NextFunction, Express } from 'express';
|
|
2
2
|
export { Router } from 'express';
|
|
3
3
|
import pg, { QueryResultRow, QueryResult, PoolClient, Pool } from 'pg';
|
|
4
4
|
import { Server } from 'node:http';
|
|
@@ -231,10 +231,10 @@ interface JobDefinition {
|
|
|
231
231
|
run: (ctx: JobContext) => Promise<void> | void;
|
|
232
232
|
}
|
|
233
233
|
declare function defineJob(name: string, run: (ctx: JobContext) => Promise<void> | void): JobDefinition;
|
|
234
|
-
declare function requirePermission(...codes: string[]): (req: Request, res: Response, next: NextFunction) => void;
|
|
235
|
-
declare function requireAuth(req: Request, res: Response, next: NextFunction): void;
|
|
236
|
-
declare function requireWorkspace(req: Request, res: Response, next: NextFunction): void;
|
|
237
|
-
declare function parseIdentity(req: Request, _res: Response, next: NextFunction): void;
|
|
234
|
+
declare function requirePermission(...codes: string[]): (req: Request, res: Response$1, next: NextFunction) => void;
|
|
235
|
+
declare function requireAuth(req: Request, res: Response$1, next: NextFunction): void;
|
|
236
|
+
declare function requireWorkspace(req: Request, res: Response$1, next: NextFunction): void;
|
|
237
|
+
declare function parseIdentity(req: Request, _res: Response$1, next: NextFunction): void;
|
|
238
238
|
declare const IDENTITY_HEADER = "x-kserp-identity";
|
|
239
239
|
declare const INTERNAL_SECRET_HEADER = "x-kserp-internal";
|
|
240
240
|
declare const PROTOCOL_VERSION = 1;
|
|
@@ -310,7 +310,7 @@ interface TenantContext {
|
|
|
310
310
|
}
|
|
311
311
|
declare function runWithTenantContext<T>(ctx: TenantContext, fn: () => T): T;
|
|
312
312
|
declare function applyTenantContext(client: PoolClient, ctx?: TenantContext | undefined): Promise<void>;
|
|
313
|
-
declare function withTenantContext(req: Request, _res: Response, next: NextFunction): void;
|
|
313
|
+
declare function withTenantContext(req: Request, _res: Response$1, next: NextFunction): void;
|
|
314
314
|
type Engine = "postgres" | "sqlite";
|
|
315
315
|
type ColumnKind = "id" | "string" | "text" | "int" | "bigint" | "float" | "decimal" | "boolean" | "timestamp" | "date" | "json" | "enum";
|
|
316
316
|
type ColumnDefault = "now" | string | number | boolean | null;
|
|
@@ -405,9 +405,9 @@ interface MigrationOpts {
|
|
|
405
405
|
useTransaction?: boolean;
|
|
406
406
|
}
|
|
407
407
|
declare function migration(name: string, up: (db: MigrationBuilder) => Promise<void>, opts?: MigrationOpts): Migration;
|
|
408
|
-
declare function asyncHandler(fn: (req: Request, res: Response, next: NextFunction) => unknown | Promise<unknown>): RequestHandler;
|
|
408
|
+
declare function asyncHandler(fn: (req: Request, res: Response$1, next: NextFunction) => unknown | Promise<unknown>): RequestHandler;
|
|
409
409
|
declare function escapeLike(s: string): string;
|
|
410
|
-
declare function checkProtocol(min?: number, max?: number): (req: Request, res: Response, next: NextFunction) => void;
|
|
410
|
+
declare function checkProtocol(min?: number, max?: number): (req: Request, res: Response$1, next: NextFunction) => void;
|
|
411
411
|
type ResourceColumns = Readonly<Record<string, ColumnDef>>;
|
|
412
412
|
type IndexMember = string | {
|
|
413
413
|
readonly ci: string;
|
|
@@ -669,6 +669,7 @@ type PluginServicesFactory = (ctx: PluginServerContext) => Record<string, Servic
|
|
|
669
669
|
interface CreatePluginServerOptions {
|
|
670
670
|
importMetaUrl: string;
|
|
671
671
|
uiOnly?: boolean;
|
|
672
|
+
flows?: readonly unknown[];
|
|
672
673
|
migrations?: boolean;
|
|
673
674
|
settings?: boolean;
|
|
674
675
|
assets?: boolean;
|
|
@@ -701,5 +702,84 @@ interface WsReceiverOptions {
|
|
|
701
702
|
host?: string;
|
|
702
703
|
}
|
|
703
704
|
declare function mountWsReceiver(opts: WsReceiverOptions): WsServer | null;
|
|
704
|
-
|
|
705
|
-
|
|
705
|
+
type FlowNodeKind = "data" | "trigger" | "modal" | "load" | "call" | "compute" | "condition" | "commit" | "emit" | "effect" | "terminal";
|
|
706
|
+
interface FlowPort {
|
|
707
|
+
id: string;
|
|
708
|
+
to: string;
|
|
709
|
+
label?: string;
|
|
710
|
+
}
|
|
711
|
+
interface FlowNodeDef {
|
|
712
|
+
id: string;
|
|
713
|
+
kind: FlowNodeKind;
|
|
714
|
+
label: string;
|
|
715
|
+
detail?: string;
|
|
716
|
+
out?: FlowPort[];
|
|
717
|
+
}
|
|
718
|
+
interface FlowDefinition {
|
|
719
|
+
id: string;
|
|
720
|
+
title: string;
|
|
721
|
+
entry?: string;
|
|
722
|
+
nodes: FlowNodeDef[];
|
|
723
|
+
}
|
|
724
|
+
declare function edge(to: string, label?: string): FlowPort;
|
|
725
|
+
declare function node(id: string, kind: FlowNodeKind, label: string, opts?: {
|
|
726
|
+
detail?: string;
|
|
727
|
+
out?: FlowPort[];
|
|
728
|
+
}): FlowNodeDef;
|
|
729
|
+
declare function defineFlow(def: FlowDefinition): FlowDefinition;
|
|
730
|
+
interface FlowNode {
|
|
731
|
+
readonly id: string;
|
|
732
|
+
to(target: FlowNode, label?: string): FlowNode;
|
|
733
|
+
}
|
|
734
|
+
declare class FlowSteps {
|
|
735
|
+
readonly prefix: string;
|
|
736
|
+
readonly nodes: FlowNodeDef[];
|
|
737
|
+
constructor(prefix: string);
|
|
738
|
+
private make;
|
|
739
|
+
trigger(label: string): FlowNode;
|
|
740
|
+
data(label: string, detail?: string): FlowNode;
|
|
741
|
+
load(label: string, detail?: string): FlowNode;
|
|
742
|
+
modal(label: string): FlowNode;
|
|
743
|
+
call(target: string, label?: string): FlowNode;
|
|
744
|
+
compute(label: string): FlowNode;
|
|
745
|
+
condition(label: string): FlowNode;
|
|
746
|
+
commit(label: string, detail?: string): FlowNode;
|
|
747
|
+
emit(event: string): FlowNode;
|
|
748
|
+
effect(label: string): FlowNode;
|
|
749
|
+
terminal(label: string): FlowNode;
|
|
750
|
+
}
|
|
751
|
+
declare function buildFlow(id: string, title: string, build: (f: FlowSteps) => void): FlowDefinition;
|
|
752
|
+
interface FlowContext {
|
|
753
|
+
state: Record<string, unknown>;
|
|
754
|
+
fetch: (url: string, init?: RequestInit) => Promise<Response>;
|
|
755
|
+
call?: (target: string, args?: unknown) => Promise<unknown>;
|
|
756
|
+
ui?: {
|
|
757
|
+
openModal?: (node: FlowNodeDef) => Promise<Record<string, unknown> | null>;
|
|
758
|
+
refresh?: () => void;
|
|
759
|
+
toast?: (msg: string) => void;
|
|
760
|
+
close?: () => void;
|
|
761
|
+
navigate?: (to: string) => void;
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
interface NodeExec {
|
|
765
|
+
request?: (ctx: FlowContext) => {
|
|
766
|
+
url: string;
|
|
767
|
+
method?: string;
|
|
768
|
+
body?: unknown;
|
|
769
|
+
};
|
|
770
|
+
when?: (ctx: FlowContext) => string | boolean;
|
|
771
|
+
compute?: (ctx: FlowContext) => unknown;
|
|
772
|
+
invoke?: {
|
|
773
|
+
target: string;
|
|
774
|
+
args?: (ctx: FlowContext) => unknown;
|
|
775
|
+
};
|
|
776
|
+
effect?: "refresh" | "toast" | "close" | "navigate";
|
|
777
|
+
arg?: string;
|
|
778
|
+
}
|
|
779
|
+
type ExecNode = FlowNodeDef & NodeExec;
|
|
780
|
+
type ExecFlow = Omit<FlowDefinition, "nodes"> & {
|
|
781
|
+
nodes: ExecNode[];
|
|
782
|
+
};
|
|
783
|
+
declare function runFlow(flow: ExecFlow, startId: string, ctx: FlowContext): Promise<void>;
|
|
784
|
+
export { FlowSteps, IDENTITY_HEADER, INTERNAL_SECRET_HEADER, MIGRATION_ADVISORY_LOCK_KEY, MigrationBuilder, PROTOCOL_VERSION, PluginUnavailableError, Types, applyTenantContext, asyncHandler, buildFlow, buildResourceRouter, buildResourceServices, callPlugin, checkInternalSecret, checkProtocol, compileCreateTable, composeWhere, createPlugin, createPluginServer, defineFlow, defineJob, defineResource, defineSchema, edge, escapeLike, identityHeaderOf, indexExpr, loadMigrations, makeDataSurface, makeDatabaseService, migration, mountPluginServices, mountWsReceiver, node, parseIdentity, quoteIdent, readIdentity, requireAuth, requirePermission, requireWorkspace, resolveEngine, resourceMigrations, rollbackMigration, runFlow, runMigrationList, runMigrations, runWithTenantContext, searchFragment, statusFilter, tenant, tryCallPlugin, verifyIdentity, withTenantContext };
|
|
785
|
+
export type { AdditionalMount, AuthProvider, AuthProviderDecl, AuthService, AuthenticatedUser, BackfillOpts, ColumnDef, ColumnDefault, ColumnKind, ColumnOpts, CompileTableOpts, CoreServices, CreatePluginServerOptions, DataSurface, DecimalOpts, DefinedResource, Engine, EscapeHatch, ExecFlow, ExecNode, FindOpts, FlowContext, FlowDefinition, FlowNode, FlowNodeDef, FlowNodeKind, FlowPort, ForwardedIdentity, Identity, IndexMember, IndexOpts, Logger, MemberDTO, Migration, MigrationContext, MigrationOpts, NodeExec, PermissionsService, PluginAsset, PluginAssets, PluginContext, PluginDb, PluginDefinition, PluginEvents, PluginInitInput, PluginInitOutput, PluginKernel, PluginManifest, PluginPeer, PluginRouterFactory, PluginRpc, PluginServerContext, PluginServerHandle, PluginServicesFactory, PluginSettings, PresignedAsset, ResourceColumns, ResourceCreateSpec, ResourceField, ResourceFields, ResourceFilter, ResourceIndex, ResourceListSpec, ResourcePagination, ResourceRouterDeps, ResourceSearch, ResourceService, ResourceSoftDelete, ResourceSort, ResourceSpec, ResourceUpdateSpec, RunMigrationsOptions, SchemaDef, SqlFragment, StringOpts, TableDef, TenantContext, TenantDb, WhereOpts, WorkspaceDTO, WsReceiverOptions };
|