okengine 0.16.0 → 0.17.0
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/manifest.v1.schema.json +21 -2
- package/package.json +1 -1
- package/site/content/docs/elements/clock.mdx +7 -0
- package/site/content/docs/elements/flow.mdx +14 -12
- package/site/content/docs/elements/gate.mdx +58 -8
- package/site/content/docs/elements/signal.mdx +46 -17
- package/site/content/docs/elements/store.mdx +23 -17
- package/site/content/docs/elements/vault.mdx +23 -0
- package/site/content/docs/get-started/project-structure.mdx +4 -4
- package/site/content/docs/reference/cli.md +1 -1
- package/site/content/docs/reference/client.mdx +72 -17
- package/site/content/docs/reference/configuration.mdx +7 -4
- package/site/content/docs/reference/errors.mdx +24 -17
- package/site/content/docs/reference/fx.mdx +15 -9
- package/src/auth/api-key-sql.ts +11 -4
- package/src/auth/api-keys.ts +3 -0
- package/src/auth/config.ts +19 -0
- package/src/auth/index.ts +37 -0
- package/src/auth/plugin.ts +6 -1
- package/src/auth/sessions.ts +18 -0
- package/src/auth/tables.ts +32 -0
- package/src/auth/tenant-config.ts +74 -0
- package/src/auth/tenant-tables.ts +11 -0
- package/src/auth/tenants.test.ts +63 -0
- package/src/auth/tenants.ts +360 -0
- package/src/cli/build.ts +2 -2
- package/src/client/budget.test.ts +1 -1
- package/src/client/create.ts +75 -5
- package/src/client/index.ts +13 -0
- package/src/client/live.test.ts +422 -0
- package/src/client/live.ts +389 -0
- package/src/client/notes-contract.test.ts +41 -0
- package/src/client/types.ts +85 -6
- package/src/client-react/index.ts +85 -1
- package/src/client-react/use-live.test.ts +129 -0
- package/src/compiler/effects-infer.ts +22 -2
- package/src/compiler/extract.test.ts +143 -11
- package/src/compiler/extract.ts +210 -30
- package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +1 -2
- package/src/compiler/fixtures/skyport.expected.json +2 -3
- package/src/compiler/response.ts +41 -11
- package/src/console/server/app.ts +44 -11
- package/src/console/server/console.test.ts +6 -8
- package/src/console/server/gates.ts +2 -9
- package/src/console/server/store.test.ts +17 -0
- package/src/console/server/store.ts +43 -1
- package/src/console/ui-next/dist/assets/{access-page-CAGHrA9H.js → access-page-CXVWWMmD.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-B-OUtiAu.js → flows-page-XDpAJO8f.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-CGoZkILK.js → index-BBj2QJCu.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-BDyXalNR.js → observability-page-BFaay44m.js} +1 -1
- package/src/console/ui-next/dist/assets/{store-page-Xh8Kn3rx.js → store-page-CS5-aETQ.js} +1 -1
- package/src/console/ui-next/dist/assets/{tree-expand-toggle-DkOXA12R.js → tree-expand-toggle-BtyhmWb4.js} +2 -1
- package/src/console/ui-next/dist/assets/{units-page-Dpk40kOQ.js → units-page-Ca2Z-E52.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-B1dB9Ft0.js → vault-page-BmOeAFwg.js} +1 -1
- package/src/console/ui-next/dist/index.html +1 -1
- package/src/console/ui-next/src/features/flows/fixture.ts +0 -1
- package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +5 -1
- package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +15 -4
- package/src/console/ui-next/ui-next-seed-manifest-surface.ts +2 -9
- package/src/console/ui-next/ui-next-seed-manifest.ts +0 -1
- package/src/drivers/index.ts +2 -0
- package/src/drivers/journal-postgres.ts +12 -3
- package/src/drivers/pg-rls.ts +26 -2
- package/src/drivers/pg-vault-rls.ts +68 -0
- package/src/drivers/signal-engine.ts +69 -15
- package/src/drivers/signal-live-iter.ts +65 -0
- package/src/drivers/signal-nats.ts +2 -1
- package/src/drivers/signal-postgres.ts +95 -12
- package/src/drivers/signal-redis.ts +2 -1
- package/src/drivers/signal-retention.ts +64 -0
- package/src/drivers/signal-types.ts +35 -5
- package/src/elements/clock/declare.ts +64 -2
- package/src/elements/clock/reconcile.ts +98 -25
- package/src/elements/clock/runtime.ts +13 -2
- package/src/elements/clock.test.ts +28 -0
- package/src/elements/clock.ts +9 -1
- package/src/elements/gate/permissions.ts +12 -0
- package/src/elements/gate.ts +6 -1
- package/src/elements/signal/declare.ts +44 -10
- package/src/elements/signal/delivery-modes.test.ts +90 -4
- package/src/elements/signal/order-lifecycle.test.ts +20 -4
- package/src/elements/signal/runtime.ts +42 -0
- package/src/elements/signal.test.ts +21 -8
- package/src/elements/signal.ts +1 -1
- package/src/elements/store/declare.ts +7 -0
- package/src/elements/store/rls-identity.test.ts +29 -0
- package/src/elements/store/rls-identity.ts +3 -0
- package/src/elements/store/schema-decl.ts +63 -3
- package/src/elements/store/schema-tenant.ts +41 -0
- package/src/elements/store/sql-rls-isolation.test.ts +39 -0
- package/src/elements/store.ts +2 -0
- package/src/elements/vault/builtin-adapter.ts +15 -2
- package/src/elements/vault/declare.ts +8 -0
- package/src/elements/vault/runtime.ts +7 -0
- package/src/elements/vault/sql-rls-isolation.test.ts +145 -0
- package/src/elements/vault/storage.ts +9 -0
- package/src/elements/vault/test-helpers.ts +2 -1
- package/src/i18n/catalogs/ar.ts +19 -0
- package/src/i18n/catalogs/en.ts +19 -0
- package/src/index.ts +1 -0
- package/src/kernel/adopt-routes.ts +56 -8
- package/src/kernel/app-tenant.ts +122 -0
- package/src/kernel/app.ts +183 -56
- package/src/kernel/auth-resolve.ts +3 -0
- package/src/kernel/boot-bind/clock.ts +9 -3
- package/src/kernel/boot.ts +2 -0
- package/src/kernel/budget.test.ts +1 -1
- package/src/kernel/clock-durable.ts +8 -0
- package/src/kernel/clock-per-tenant-name.ts +5 -0
- package/src/kernel/clock-reconcile.ts +8 -0
- package/src/kernel/errors-live-resume.ts +15 -0
- package/src/kernel/errors-tenant.ts +29 -0
- package/src/kernel/errors.registry.test.ts +31 -3
- package/src/kernel/errors.ts +43 -3
- package/src/kernel/flow.ts +26 -5
- package/src/kernel/fx-auth-keys.ts +6 -1
- package/src/kernel/fx-auth-tenants.test.ts +87 -0
- package/src/kernel/fx-auth-tenants.ts +286 -0
- package/src/kernel/fx-live-stream.ts +149 -0
- package/src/kernel/fx-live.test.ts +157 -0
- package/src/kernel/fx-runtime.ts +15 -0
- package/src/kernel/fx-tenant-store.ts +213 -0
- package/src/kernel/fx.test.ts +91 -0
- package/src/kernel/fx.ts +173 -13
- package/src/kernel/hooks.ts +2 -2
- package/src/kernel/http-resource.ts +9 -18
- package/src/kernel/index.ts +2 -0
- package/src/kernel/journal.ts +12 -0
- package/src/kernel/live-http.test.ts +78 -0
- package/src/kernel/live-http.ts +114 -0
- package/src/kernel/live-resume.test.ts +125 -0
- package/src/kernel/on.ts +51 -0
- package/src/kernel/pipeline-tenant.ts +49 -0
- package/src/kernel/pipeline.test.ts +1 -1
- package/src/kernel/pipeline.ts +37 -2
- package/src/kernel/resource-mount.test.ts +10 -27
- package/src/kernel/tenant-resolve.test.ts +101 -0
- package/src/kernel/tenant-resolve.ts +124 -0
- package/src/kernel/tenant-roles.test.ts +87 -0
- package/src/kernel/triggers.ts +59 -21
- package/src/manifest/diff.test.ts +11 -2
- package/src/manifest/diff.ts +53 -11
- package/src/manifest/fixtures/skyport.excerpt.json +1 -1
- package/src/manifest/fixtures/skyport.manifest.json +0 -1
- package/src/manifest/types.ts +52 -6
- package/src/release/build-lib.ts +13 -1
- package/src/release/limits.ts +2 -2
- package/src/release/measure.ts +55 -1
- package/src/client/live-gap.test.ts +0 -35
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useLive — mount/unmount and typed event state.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
|
|
6
|
+
import { Window } from "happy-dom";
|
|
7
|
+
import { act, createElement } from "react";
|
|
8
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
9
|
+
import { createClient } from "../client/create.ts";
|
|
10
|
+
import type { AppOf, LiveSignalHandle } from "../client/types.ts";
|
|
11
|
+
import { useLive } from "./index.ts";
|
|
12
|
+
|
|
13
|
+
type EventsApp = AppOf<{
|
|
14
|
+
orders: {
|
|
15
|
+
events: {
|
|
16
|
+
in: { orderId: string };
|
|
17
|
+
out: { orderId: string; status: string };
|
|
18
|
+
method: "GET";
|
|
19
|
+
path: "/orders/:orderId/events";
|
|
20
|
+
live: "order-status";
|
|
21
|
+
matchKey: ["orderId"];
|
|
22
|
+
stream: true;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
}>;
|
|
26
|
+
|
|
27
|
+
type Payload = { orderId: string; status: string };
|
|
28
|
+
|
|
29
|
+
const orderStatus: LiveSignalHandle<Payload> = {
|
|
30
|
+
name: "order-status",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function sseResponse(frames: readonly unknown[], ids?: readonly string[]): Response {
|
|
34
|
+
const lines: string[] = [];
|
|
35
|
+
frames.forEach((payload, i) => {
|
|
36
|
+
const id = ids?.[i];
|
|
37
|
+
if (id !== undefined) lines.push(`id: ${id}`);
|
|
38
|
+
lines.push(`data: ${JSON.stringify(payload)}`);
|
|
39
|
+
lines.push("");
|
|
40
|
+
});
|
|
41
|
+
lines.push("data: [DONE]");
|
|
42
|
+
lines.push("");
|
|
43
|
+
return new Response(lines.join("\n"), {
|
|
44
|
+
status: 200,
|
|
45
|
+
headers: { "content-type": "text/event-stream" },
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function waitFor(pred: () => boolean, ms = 1_000): Promise<void> {
|
|
50
|
+
const start = Date.now();
|
|
51
|
+
while (!pred()) {
|
|
52
|
+
if (Date.now() - start > ms) throw new Error("timeout");
|
|
53
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe("useLive", () => {
|
|
58
|
+
let happy: Window;
|
|
59
|
+
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
62
|
+
happy = new Window({ url: "http://app.test/" });
|
|
63
|
+
Object.defineProperty(globalThis, "window", {
|
|
64
|
+
value: happy,
|
|
65
|
+
configurable: true,
|
|
66
|
+
writable: true,
|
|
67
|
+
});
|
|
68
|
+
Object.defineProperty(globalThis, "document", {
|
|
69
|
+
value: happy.document,
|
|
70
|
+
configurable: true,
|
|
71
|
+
writable: true,
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
afterEach(() => {
|
|
76
|
+
happy.close();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("collects events and latest; unmount stops fetches", async () => {
|
|
80
|
+
let calls = 0;
|
|
81
|
+
const payload = { orderId: "ord_1", status: "placed" as const };
|
|
82
|
+
const api = createClient<EventsApp>("http://app.test", {
|
|
83
|
+
fetch: async () => {
|
|
84
|
+
calls += 1;
|
|
85
|
+
return sseResponse([payload], ["evt-1"]);
|
|
86
|
+
},
|
|
87
|
+
$routes: {
|
|
88
|
+
orders: {
|
|
89
|
+
events: {
|
|
90
|
+
method: "GET",
|
|
91
|
+
path: "/orders/:orderId/events",
|
|
92
|
+
live: "order-status",
|
|
93
|
+
matchKey: ["orderId"],
|
|
94
|
+
stream: true,
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const host = happy.document.createElement("div");
|
|
101
|
+
happy.document.body.appendChild(host);
|
|
102
|
+
const seen: { latest: Payload | null; events: Payload[] } = { latest: null, events: [] };
|
|
103
|
+
|
|
104
|
+
function Probe(): null {
|
|
105
|
+
const state = useLive(api, orderStatus, { orderId: "ord_1" });
|
|
106
|
+
seen.latest = state.latest;
|
|
107
|
+
seen.events = state.events;
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let root: Root | undefined;
|
|
112
|
+
await act(async () => {
|
|
113
|
+
root = createRoot(host as unknown as Element);
|
|
114
|
+
root.render(createElement(Probe));
|
|
115
|
+
});
|
|
116
|
+
await waitFor(() => seen.events.length === 1);
|
|
117
|
+
expect(seen.latest).toEqual(payload);
|
|
118
|
+
expect(seen.events).toEqual([payload]);
|
|
119
|
+
expect(calls).toBe(1);
|
|
120
|
+
|
|
121
|
+
await act(async () => {
|
|
122
|
+
root?.unmount();
|
|
123
|
+
});
|
|
124
|
+
const after = calls;
|
|
125
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
126
|
+
expect(calls).toBe(after);
|
|
127
|
+
host.remove();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import type {
|
|
10
10
|
AuthApiKeysResourceRef,
|
|
11
|
+
AuthTenantsResourceRef,
|
|
11
12
|
Effects,
|
|
12
13
|
PromptRef,
|
|
13
14
|
ResourceRef,
|
|
@@ -146,8 +147,10 @@ const TABLE_ARG_METHODS = new Set([
|
|
|
146
147
|
* @param options - Handler AST, bindings, and annotation flag
|
|
147
148
|
*/
|
|
148
149
|
export function inferEffects(options: InferEffectsOptions): InferredEffects {
|
|
149
|
-
const reads = new Set<
|
|
150
|
-
|
|
150
|
+
const reads = new Set<
|
|
151
|
+
ResourceRef | SignalResourceRef | AuthApiKeysResourceRef | AuthTenantsResourceRef
|
|
152
|
+
>();
|
|
153
|
+
const writes = new Set<ResourceRef | AuthApiKeysResourceRef | AuthTenantsResourceRef>();
|
|
151
154
|
const emits = new Set<SignalRef>();
|
|
152
155
|
const sends = new Set<TemplateRef>();
|
|
153
156
|
const asks = new Set<PromptRef>();
|
|
@@ -183,6 +186,12 @@ export function inferEffects(options: InferEffectsOptions): InferredEffects {
|
|
|
183
186
|
continue;
|
|
184
187
|
}
|
|
185
188
|
|
|
189
|
+
if (chain.rootMethod === "live" && call === chain.rootCall) {
|
|
190
|
+
const ref = resolveNamed(call.arguments[0], options.bindings, "signal");
|
|
191
|
+
if (ref) reads.add(`signal:${ref}`);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
186
195
|
if (chain.rootMethod === "send" && call === chain.rootCall) {
|
|
187
196
|
const ref = resolveNamed(call.arguments[0], options.bindings, "template");
|
|
188
197
|
if (ref) sends.add(ref);
|
|
@@ -221,6 +230,7 @@ export function inferEffects(options: InferEffectsOptions): InferredEffects {
|
|
|
221
230
|
if (chain.rootMethod === "auth" && call === chain.rootCall) {
|
|
222
231
|
const method = chain.methods[1];
|
|
223
232
|
if (method === "listApiKeys") reads.add("auth:api-keys");
|
|
233
|
+
if (method === "listTenants" || method === "listMembers") reads.add("auth:tenants");
|
|
224
234
|
if (
|
|
225
235
|
method === "createApiKey" ||
|
|
226
236
|
method === "revokeApiKey" ||
|
|
@@ -229,6 +239,16 @@ export function inferEffects(options: InferEffectsOptions): InferredEffects {
|
|
|
229
239
|
) {
|
|
230
240
|
writes.add("auth:api-keys");
|
|
231
241
|
}
|
|
242
|
+
if (
|
|
243
|
+
method === "switchTenant" ||
|
|
244
|
+
method === "createTenant" ||
|
|
245
|
+
method === "deleteTenant" ||
|
|
246
|
+
method === "addMember" ||
|
|
247
|
+
method === "removeMember" ||
|
|
248
|
+
method === "upsertTenantRole"
|
|
249
|
+
) {
|
|
250
|
+
writes.add("auth:tenants");
|
|
251
|
+
}
|
|
232
252
|
continue;
|
|
233
253
|
}
|
|
234
254
|
|
|
@@ -337,6 +337,23 @@ export const stripeKey = vault.secret("STRIPE_KEY", {
|
|
|
337
337
|
expect(manifest.vault?.STRIPE_KEY?.description).toBe("Payments gateway key");
|
|
338
338
|
});
|
|
339
339
|
|
|
340
|
+
test("extracts live signal retention", async () => {
|
|
341
|
+
const manifest = await extractFromSources({
|
|
342
|
+
"src/signals.ts": `
|
|
343
|
+
import { signal } from "okengine";
|
|
344
|
+
export const orderStatus = signal("order-status", {
|
|
345
|
+
delivery: "live",
|
|
346
|
+
optional: true,
|
|
347
|
+
retention: { maxAge: "24h", maxCount: 500 },
|
|
348
|
+
});
|
|
349
|
+
`,
|
|
350
|
+
});
|
|
351
|
+
expect(manifest.signals?.["order-status"]?.retention).toEqual({
|
|
352
|
+
maxAge: "24h",
|
|
353
|
+
maxCount: 500,
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
340
357
|
test("extracts store.kv durable onto the Manifest store", async () => {
|
|
341
358
|
const manifest = await extractFromSources({
|
|
342
359
|
"src/kv.ts": `
|
|
@@ -362,7 +379,6 @@ export const { list, create, get, update, remove } = bindNamedTableCrud({
|
|
|
362
379
|
unit: "cycles",
|
|
363
380
|
path: "/cycles",
|
|
364
381
|
table: cycles,
|
|
365
|
-
liveList: true,
|
|
366
382
|
});
|
|
367
383
|
`;
|
|
368
384
|
const helper = `
|
|
@@ -379,7 +395,7 @@ export function bindNamedTableCrud(spec) {
|
|
|
379
395
|
expect(manifest.flows?.["cycles.list"]?.trigger).toEqual({
|
|
380
396
|
http: { method: "GET", path: "/cycles" },
|
|
381
397
|
});
|
|
382
|
-
expect(manifest.flows?.["cycles.list"]?.live).
|
|
398
|
+
expect(manifest.flows?.["cycles.list"]?.live).toBeUndefined();
|
|
383
399
|
expect(manifest.flows?.["cycles.create"]?.trigger).toEqual({
|
|
384
400
|
http: { method: "POST", path: "/cycles" },
|
|
385
401
|
});
|
|
@@ -469,7 +485,7 @@ export const remove = mounted.remove;
|
|
|
469
485
|
expect(manifest.flows?.list?.breaking).toBe(true);
|
|
470
486
|
});
|
|
471
487
|
|
|
472
|
-
test("chains .gate(...)
|
|
488
|
+
test("chains .gate(...) onto the five verbs", async () => {
|
|
473
489
|
const source = `
|
|
474
490
|
import { on, http, store, gate } from "okengine";
|
|
475
491
|
|
|
@@ -478,7 +494,7 @@ export const db = store.sql("notes", { schema: {} });
|
|
|
478
494
|
|
|
479
495
|
const notesR = store.resource(db, {}, {});
|
|
480
496
|
|
|
481
|
-
const mounted = on(http.resource("/notes", notesR.all()).gate(member)
|
|
497
|
+
const mounted = on(http.resource("/notes", notesR.all()).gate(member));
|
|
482
498
|
|
|
483
499
|
export const list = mounted.list;
|
|
484
500
|
export const create = mounted.create;
|
|
@@ -493,20 +509,18 @@ export const remove = mounted.remove;
|
|
|
493
509
|
expect(manifest.flows?.get?.gates).toEqual(["member"]);
|
|
494
510
|
expect(manifest.flows?.update?.gates).toEqual(["member"]);
|
|
495
511
|
expect(manifest.flows?.remove?.gates).toEqual(["member"]);
|
|
496
|
-
expect(manifest.flows?.list?.live).
|
|
497
|
-
expect(manifest.flows?.get?.live).
|
|
512
|
+
expect(manifest.flows?.list?.live).toBeUndefined();
|
|
513
|
+
expect(manifest.flows?.get?.live).toBeUndefined();
|
|
498
514
|
expect(manifest.flows?.create?.live).toBeUndefined();
|
|
499
|
-
expect(manifest.flows?.update?.live).toBeUndefined();
|
|
500
|
-
expect(manifest.flows?.remove?.live).toBeUndefined();
|
|
501
515
|
});
|
|
502
516
|
|
|
503
|
-
test("chains .public()
|
|
517
|
+
test("chains .public() onto the five verbs", async () => {
|
|
504
518
|
const source = `
|
|
505
519
|
import { on, http, store } from "okengine";
|
|
506
520
|
|
|
507
521
|
export const db = store.sql("notes", { schema: {} });
|
|
508
522
|
const notesR = store.resource(db, {}, {});
|
|
509
|
-
const mounted = on(http.resource("/notes", notesR.all()).public()
|
|
523
|
+
const mounted = on(http.resource("/notes", notesR.all()).public());
|
|
510
524
|
|
|
511
525
|
export const list = mounted.list;
|
|
512
526
|
export const create = mounted.create;
|
|
@@ -517,7 +531,7 @@ export const remove = mounted.remove;
|
|
|
517
531
|
const manifest = await extractFromSources({ "src/flows/notes.ts": source });
|
|
518
532
|
expect(manifest.flows?.list?.gates).toEqual(["public"]);
|
|
519
533
|
expect(manifest.flows?.create?.gates).toEqual(["public"]);
|
|
520
|
-
expect(manifest.flows?.get?.live).
|
|
534
|
+
expect(manifest.flows?.get?.live).toBeUndefined();
|
|
521
535
|
expect(manifest.flows?.create?.live).toBeUndefined();
|
|
522
536
|
});
|
|
523
537
|
});
|
|
@@ -672,6 +686,67 @@ export const failed = on(
|
|
|
672
686
|
});
|
|
673
687
|
});
|
|
674
688
|
|
|
689
|
+
describe("extractManifest — fx.live / .live(signal)", () => {
|
|
690
|
+
test("one-arg on(http.get(path).live(signal)) stamps live + signal read", async () => {
|
|
691
|
+
const source = `
|
|
692
|
+
import { on, http, gate, signal } from "okengine";
|
|
693
|
+
import { z } from "zod";
|
|
694
|
+
|
|
695
|
+
export const member = gate.policy("member", ({ auth }) => !!auth.verified);
|
|
696
|
+
export const orderStatus = signal("order-status", {
|
|
697
|
+
delivery: "live",
|
|
698
|
+
optional: true,
|
|
699
|
+
schema: z.object({ orderId: z.string(), status: z.string() }),
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
export const events = on(http.get("/orders/:orderId/events").gate(member).live(orderStatus));
|
|
703
|
+
`;
|
|
704
|
+
const manifest = await extractFromSources({ "src/flows/orders/index.ts": source });
|
|
705
|
+
const flow = manifest.flows?.["orders.events"];
|
|
706
|
+
expect(flow?.live).toBe("order-status");
|
|
707
|
+
expect(flow?.trigger).toEqual({
|
|
708
|
+
http: { method: "GET", path: "/orders/:orderId/events" },
|
|
709
|
+
});
|
|
710
|
+
expect(flow?.gates).toEqual(["member"]);
|
|
711
|
+
expect(flow?.effects?.reads).toEqual(["signal:order-status"]);
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
test("http.live(signal) uses GET /_oke/live/{name}", async () => {
|
|
715
|
+
const source = `
|
|
716
|
+
import { on, http, gate, signal } from "okengine";
|
|
717
|
+
|
|
718
|
+
export const member = gate.policy("member", ({ auth }) => !!auth.verified);
|
|
719
|
+
export const orderStatus = signal("order-status", { delivery: "live", optional: true });
|
|
720
|
+
|
|
721
|
+
export const firehose = on(http.live(orderStatus).gate(member));
|
|
722
|
+
`;
|
|
723
|
+
const manifest = await extractFromSources({ "src/flows/orders/index.ts": source });
|
|
724
|
+
const flow = manifest.flows?.["orders.firehose"];
|
|
725
|
+
expect(flow?.live).toBe("order-status");
|
|
726
|
+
expect(flow?.trigger).toEqual({
|
|
727
|
+
http: { method: "GET", path: "/_oke/live/order-status" },
|
|
728
|
+
});
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
test("fx.live(signal) infers reads: [signal:<name>]", async () => {
|
|
732
|
+
const source = `
|
|
733
|
+
import { on, flow, http, signal } from "okengine";
|
|
734
|
+
|
|
735
|
+
export const orderStatus = signal("order-status", { delivery: "live", optional: true });
|
|
736
|
+
|
|
737
|
+
export const events = on(
|
|
738
|
+
http.get("/feed").public().live(orderStatus),
|
|
739
|
+
flow("orders.events", {
|
|
740
|
+
do: (_input, fx) => fx.live(orderStatus),
|
|
741
|
+
}),
|
|
742
|
+
);
|
|
743
|
+
`;
|
|
744
|
+
const manifest = await extractFromSources({ "src/flows/orders.ts": source });
|
|
745
|
+
expect(manifest.flows?.["orders.events"]?.live).toBe("order-status");
|
|
746
|
+
expect(manifest.flows?.["orders.events"]?.effects?.reads).toEqual(["signal:order-status"]);
|
|
747
|
+
});
|
|
748
|
+
});
|
|
749
|
+
|
|
675
750
|
describe("extractManifest — channel medium binder aliasing", () => {
|
|
676
751
|
test("mail.template(...) resolves through `const mail = channel.email(...)`", async () => {
|
|
677
752
|
const source = `
|
|
@@ -1205,3 +1280,60 @@ export const create = flow("views.create", {
|
|
|
1205
1280
|
});
|
|
1206
1281
|
});
|
|
1207
1282
|
});
|
|
1283
|
+
|
|
1284
|
+
describe("extractManifest — tenancy", () => {
|
|
1285
|
+
test("oke({ gate: { auth: { tenant: true }}}) stamps tenancy.enabled", async () => {
|
|
1286
|
+
const source = `
|
|
1287
|
+
import { oke, on, flow, http, store, field } from "okengine";
|
|
1288
|
+
|
|
1289
|
+
export const bookings = store.schema.table("bookings", {
|
|
1290
|
+
id: field.text().primaryKey(),
|
|
1291
|
+
tenantId: field.text().notNull(),
|
|
1292
|
+
}, [
|
|
1293
|
+
store.schema.policy.tenant("tenant_id"),
|
|
1294
|
+
]);
|
|
1295
|
+
|
|
1296
|
+
export const db = store.sql("app", { schema: { bookings } });
|
|
1297
|
+
|
|
1298
|
+
export const ping = on(
|
|
1299
|
+
http.get("/ping").public(),
|
|
1300
|
+
flow("health.ping", { do: () => ({ ok: true }) }),
|
|
1301
|
+
);
|
|
1302
|
+
|
|
1303
|
+
export const app = oke({
|
|
1304
|
+
name: "shop",
|
|
1305
|
+
gate: { auth: { tenant: true } },
|
|
1306
|
+
});
|
|
1307
|
+
`;
|
|
1308
|
+
const manifest = await extractFromSources({ "src/app.ts": source });
|
|
1309
|
+
expect(manifest.tenancy).toMatchObject({
|
|
1310
|
+
enabled: true,
|
|
1311
|
+
isolation: "row",
|
|
1312
|
+
membershipRequired: false,
|
|
1313
|
+
});
|
|
1314
|
+
expect(manifest.flows?.["health.ping"]?.tenantScoped).toBe(true);
|
|
1315
|
+
expect(manifest.stores?.app?.tables?.bookings?.policies?.tenant_tenant_id_all?.using).toContain(
|
|
1316
|
+
"oke.tenant()",
|
|
1317
|
+
);
|
|
1318
|
+
});
|
|
1319
|
+
|
|
1320
|
+
test("SQL table without tenant policy or unscoped() fails loud", async () => {
|
|
1321
|
+
const source = `
|
|
1322
|
+
import { oke, store, field } from "okengine";
|
|
1323
|
+
|
|
1324
|
+
export const notes = store.schema.table("notes", {
|
|
1325
|
+
id: field.text().primaryKey(),
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1328
|
+
export const db = store.sql("app", { schema: { notes } });
|
|
1329
|
+
|
|
1330
|
+
export const app = oke({
|
|
1331
|
+
name: "shop",
|
|
1332
|
+
gate: { auth: { tenant: true } },
|
|
1333
|
+
});
|
|
1334
|
+
`;
|
|
1335
|
+
await expect(extractFromSources({ "src/app.ts": source })).rejects.toThrow(
|
|
1336
|
+
/needs store.schema.policy.tenant/,
|
|
1337
|
+
);
|
|
1338
|
+
});
|
|
1339
|
+
});
|