@kahitsan/plugin-sdk 0.1.0-staging.8 → 0.2.0-staging.27

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.
@@ -0,0 +1,2 @@
1
+ export { buildFlow, FlowSteps, runFlow, defineFlow, node, edge } from "./index.js";
2
+ export type { FlowDefinition, FlowNodeDef, FlowNodeKind, FlowPort, FlowNode, ExecFlow, ExecNode, FlowContext, NodeExec } from "./index.js";
@@ -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
- export { IDENTITY_HEADER, INTERNAL_SECRET_HEADER, MIGRATION_ADVISORY_LOCK_KEY, MigrationBuilder, PROTOCOL_VERSION, PluginUnavailableError, Types, applyTenantContext, asyncHandler, buildResourceRouter, buildResourceServices, callPlugin, checkInternalSecret, checkProtocol, compileCreateTable, composeWhere, createPlugin, createPluginServer, defineJob, defineResource, defineSchema, escapeLike, identityHeaderOf, indexExpr, loadMigrations, makeDataSurface, makeDatabaseService, migration, mountPluginServices, mountWsReceiver, parseIdentity, quoteIdent, readIdentity, requireAuth, requirePermission, requireWorkspace, resolveEngine, resourceMigrations, rollbackMigration, runMigrationList, runMigrations, runWithTenantContext, searchFragment, statusFilter, tenant, tryCallPlugin, verifyIdentity, withTenantContext };
705
- export type { AdditionalMount, AuthProvider, AuthProviderDecl, AuthService, AuthenticatedUser, BackfillOpts, ColumnDef, ColumnDefault, ColumnKind, ColumnOpts, CompileTableOpts, CoreServices, CreatePluginServerOptions, DataSurface, DecimalOpts, DefinedResource, Engine, EscapeHatch, FindOpts, ForwardedIdentity, Identity, IndexMember, IndexOpts, Logger, MemberDTO, Migration, MigrationContext, MigrationOpts, 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 };
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 };