@supacloud/elysia 0.5.1 → 0.7.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/README.md +42 -0
- package/dist/index.d.ts +14 -2
- package/dist/index.js +324 -2
- package/dist/memory.d.ts +68 -0
- package/dist/memory_policy.d.ts +11 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,6 +15,9 @@ Runtime adapter that turns `@supacloud/compiler` output into a production-ready
|
|
|
15
15
|
`destroyRequestScope` after the response, including when the handler fails.
|
|
16
16
|
- **TypeBox schema binding**: attaches compiled parameter, query, body, and
|
|
17
17
|
response TypeBox schemas directly to Elysia route definitions.
|
|
18
|
+
- **Compiler invoker execution**: uses the compiler-emitted positional invoker
|
|
19
|
+
after Elysia has decoded route input, while retaining the legacy input-object
|
|
20
|
+
handler path for hand-written compiled fixtures.
|
|
18
21
|
- **Unified Command Pipeline**: runs `@Command`-decorated handlers through a
|
|
19
22
|
structured `commandGovernance` adapter chain or a custom `composeCommandExecutors`
|
|
20
23
|
pipeline (fail-closed if command routes lack an executor).
|
|
@@ -58,6 +61,31 @@ const app = createApplication({
|
|
|
58
61
|
export default app;
|
|
59
62
|
```
|
|
60
63
|
|
|
64
|
+
For deterministic local verification, use the in-memory sandbox. It supplies
|
|
65
|
+
stable request identity, an isolated key-value database with optimistic
|
|
66
|
+
transaction rollback, and an in-memory object store without requiring
|
|
67
|
+
PostgreSQL, GoTrue, or S3:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { createMemorySandbox } from "@supacloud/elysia";
|
|
71
|
+
|
|
72
|
+
const sandbox = createMemorySandbox({
|
|
73
|
+
modules: [CaseModule],
|
|
74
|
+
identity: { authenticated: true, subject: "test-user" },
|
|
75
|
+
requestId: "test-request",
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const response = await sandbox.request("/cases/42");
|
|
79
|
+
sandbox.db.set("cases", "42", { state: "draft" });
|
|
80
|
+
sandbox.storage.put("evidence", "42.txt", "fixture");
|
|
81
|
+
sandbox.reset();
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Route `body`, `params`, `query`, and `response` schemas are enforced by
|
|
85
|
+
Elysia before and after the handler. Invalid input returns the standard `422`
|
|
86
|
+
validation response; invalid handler output is rejected before it reaches the
|
|
87
|
+
client.
|
|
88
|
+
|
|
61
89
|
Jobs are executed explicitly with `executeJob(compiledModule, services, job,
|
|
62
90
|
input, requestContext)`. The asynchronous compiler-generated job scope is
|
|
63
91
|
destroyed after execution, including when the job throws or scope construction
|
|
@@ -92,3 +120,17 @@ compiler-generated job scope.
|
|
|
92
120
|
|
|
93
121
|
Lightweight error class carrying HTTP `status`, machine-readable `code`, and
|
|
94
122
|
optional structured `details`.
|
|
123
|
+
|
|
124
|
+
### `createMemorySandbox(options): MemorySandbox`
|
|
125
|
+
|
|
126
|
+
Creates an in-process application harness with `request()`, `db`, `storage`,
|
|
127
|
+
`identity`, `policy`, `audit`, and `reset()`. The memory database is a deterministic test adapter,
|
|
128
|
+
not a PostgreSQL emulator; its transaction callback operates on an isolated
|
|
129
|
+
snapshot and detects concurrent commits.
|
|
130
|
+
|
|
131
|
+
The adapter boundary is intentional: production authorization, RLS, PostgreSQL,
|
|
132
|
+
S3 visibility and failure semantics must be supplied by application governance
|
|
133
|
+
adapters. The memory harness is limited to deterministic HTTP, key-value
|
|
134
|
+
transaction and object-storage contract tests.
|
|
135
|
+
`policy` supplies explicit permission grants/revocations and idempotency claims;
|
|
136
|
+
`storage.failNext()` makes storage failure paths deterministic.
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,14 @@ export interface CompiledRoute {
|
|
|
9
9
|
params?: unknown;
|
|
10
10
|
query?: unknown;
|
|
11
11
|
response?: unknown;
|
|
12
|
+
/** Compiler-emitted positional invoker; used when available. */
|
|
13
|
+
invoker?: (controller: unknown, request: {
|
|
14
|
+
params?: Record<string, unknown>;
|
|
15
|
+
query?: Record<string, unknown>;
|
|
16
|
+
body?: unknown;
|
|
17
|
+
headers?: Record<string, unknown>;
|
|
18
|
+
context?: unknown;
|
|
19
|
+
}) => Promise<unknown> | unknown;
|
|
12
20
|
/** Class name of the @Command explicitly bound to this route. */
|
|
13
21
|
command?: string;
|
|
14
22
|
/** Statically generated route aspects. */
|
|
@@ -73,7 +81,7 @@ export interface CommandInvocation {
|
|
|
73
81
|
command: CompiledCommand;
|
|
74
82
|
input: {
|
|
75
83
|
body: unknown;
|
|
76
|
-
params: Record<string,
|
|
84
|
+
params: Record<string, unknown>;
|
|
77
85
|
query: Record<string, unknown>;
|
|
78
86
|
};
|
|
79
87
|
request: Request;
|
|
@@ -143,7 +151,7 @@ export type ErrorMapper = (error: unknown, context: ErrorContext) => Response |
|
|
|
143
151
|
export interface ApplicationOptions {
|
|
144
152
|
name?: string;
|
|
145
153
|
/** Modules in topological import order. */
|
|
146
|
-
modules
|
|
154
|
+
modules?: CompiledModule[];
|
|
147
155
|
/** Platform-level dependencies (db client etc.), passed to createServices. */
|
|
148
156
|
deps?: Record<string, unknown>;
|
|
149
157
|
/** Builds the per-request context object. Defaults to { requestId, request }. */
|
|
@@ -205,3 +213,7 @@ export declare function defaultErrorResponse(error: unknown, frameworkCode?: str
|
|
|
205
213
|
export declare function createApplication(options: ApplicationOptions): Elysia;
|
|
206
214
|
/** Semantic alias of createApplication for readable tests. */
|
|
207
215
|
export declare function createTestApp(options: ApplicationOptions): Elysia;
|
|
216
|
+
export { createMemorySandbox, } from "./memory";
|
|
217
|
+
export type { HandleLike as MemoryHandleLike, MemoryDatabase, MemorySandbox, MemorySandboxOptions, MemoryStorage, MemoryStorageObject, } from "./memory";
|
|
218
|
+
export { createMemoryPolicy } from "./memory_policy";
|
|
219
|
+
export type { MemoryPolicy } from "./memory_policy";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,323 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { Elysia } from "elysia";
|
|
3
|
+
|
|
4
|
+
// src/memory.ts
|
|
5
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
|
+
|
|
7
|
+
// src/memory_policy.ts
|
|
8
|
+
function createMemoryPolicy() {
|
|
9
|
+
const permissions = new Map;
|
|
10
|
+
const receipts = new Map;
|
|
11
|
+
return {
|
|
12
|
+
grant(subject, permission) {
|
|
13
|
+
const values = permissions.get(subject) ?? new Set;
|
|
14
|
+
values.add(permission);
|
|
15
|
+
permissions.set(subject, values);
|
|
16
|
+
},
|
|
17
|
+
revoke(subject, permission) {
|
|
18
|
+
permissions.get(subject)?.delete(permission);
|
|
19
|
+
},
|
|
20
|
+
can(subject, permission) {
|
|
21
|
+
return permissions.get(subject)?.has(permission) ?? false;
|
|
22
|
+
},
|
|
23
|
+
claimIdempotency(subject, key) {
|
|
24
|
+
const id = JSON.stringify([subject, key]);
|
|
25
|
+
if (receipts.has(id))
|
|
26
|
+
return false;
|
|
27
|
+
receipts.set(id, { input: "", result: Promise.resolve(undefined) });
|
|
28
|
+
return true;
|
|
29
|
+
},
|
|
30
|
+
subject(context) {
|
|
31
|
+
if (typeof context === "object" && context !== null && "identity" in context) {
|
|
32
|
+
const identity = context.identity;
|
|
33
|
+
if (typeof identity === "object" && identity !== null && "authenticated" in identity && identity.authenticated === true && "subject" in identity && typeof identity.subject === "string" && identity.subject.length > 0)
|
|
34
|
+
return identity.subject;
|
|
35
|
+
}
|
|
36
|
+
throw new ApplicationError("Memory authenticated identity required", { status: 401, code: "MEMORY_IDENTITY_REQUIRED" });
|
|
37
|
+
},
|
|
38
|
+
async runOnce(subject, command, key, input, work) {
|
|
39
|
+
if (!subject || !command || !key)
|
|
40
|
+
throw new Error("Memory receipt requires subject, command and key");
|
|
41
|
+
const id = JSON.stringify([subject, command, key]);
|
|
42
|
+
const fingerprint = stableStringify(input);
|
|
43
|
+
const existing = receipts.get(id);
|
|
44
|
+
if (existing) {
|
|
45
|
+
if (existing.input !== fingerprint) {
|
|
46
|
+
throw new ApplicationError("Idempotency input conflict", { status: 409, code: "MEMORY_IDEMPOTENCY_CONFLICT" });
|
|
47
|
+
}
|
|
48
|
+
return structuredClone(await existing.result);
|
|
49
|
+
}
|
|
50
|
+
const result = Promise.resolve().then(work).then((value) => structuredClone(value));
|
|
51
|
+
const entry = { input: fingerprint, result };
|
|
52
|
+
receipts.set(id, entry);
|
|
53
|
+
try {
|
|
54
|
+
return structuredClone(await result);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (receipts.get(id) === entry)
|
|
57
|
+
receipts.delete(id);
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
clear() {
|
|
62
|
+
permissions.clear();
|
|
63
|
+
receipts.clear();
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function stableStringify(value) {
|
|
68
|
+
if (value === null || typeof value !== "object")
|
|
69
|
+
return JSON.stringify(value);
|
|
70
|
+
if (Array.isArray(value))
|
|
71
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
72
|
+
const record = value;
|
|
73
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`).join(",")}}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/memory.ts
|
|
77
|
+
class DefaultMemoryDatabase {
|
|
78
|
+
tables = new Map;
|
|
79
|
+
version = 0;
|
|
80
|
+
current = new AsyncLocalStorage;
|
|
81
|
+
get(table, key) {
|
|
82
|
+
if (this.current.getStore())
|
|
83
|
+
return this.current.getStore()?.get(table, key);
|
|
84
|
+
const value = this.tables.get(table)?.get(key);
|
|
85
|
+
return value === undefined ? undefined : cloneValue(value);
|
|
86
|
+
}
|
|
87
|
+
set(table, key, value) {
|
|
88
|
+
const current = this.current.getStore();
|
|
89
|
+
if (current)
|
|
90
|
+
return current.set(table, key, value);
|
|
91
|
+
const rows = this.tables.get(table) ?? new Map;
|
|
92
|
+
rows.set(key, cloneValue(value));
|
|
93
|
+
this.tables.set(table, rows);
|
|
94
|
+
this.version += 1;
|
|
95
|
+
}
|
|
96
|
+
delete(table, key) {
|
|
97
|
+
const current = this.current.getStore();
|
|
98
|
+
if (current)
|
|
99
|
+
return current.delete(table, key);
|
|
100
|
+
const deleted = this.tables.get(table)?.delete(key) ?? false;
|
|
101
|
+
if (deleted)
|
|
102
|
+
this.version += 1;
|
|
103
|
+
return deleted;
|
|
104
|
+
}
|
|
105
|
+
list(table) {
|
|
106
|
+
const current = this.current.getStore();
|
|
107
|
+
if (current)
|
|
108
|
+
return current.list(table);
|
|
109
|
+
return [...this.tables.get(table)?.entries() ?? []].map(([key, value]) => ({ key, value: cloneValue(value) })).sort((left, right) => left.key.localeCompare(right.key));
|
|
110
|
+
}
|
|
111
|
+
clear() {
|
|
112
|
+
const current = this.current.getStore();
|
|
113
|
+
if (current)
|
|
114
|
+
return current.clear();
|
|
115
|
+
this.tables.clear();
|
|
116
|
+
this.version += 1;
|
|
117
|
+
}
|
|
118
|
+
async transaction(work) {
|
|
119
|
+
if (this.current.getStore())
|
|
120
|
+
throw new Error("Nested memory transactions are not supported");
|
|
121
|
+
const baseVersion = this.version;
|
|
122
|
+
const snapshot = cloneTables(this.tables);
|
|
123
|
+
const transaction = new SnapshotMemoryDatabase(snapshot);
|
|
124
|
+
try {
|
|
125
|
+
const result = await this.current.run(transaction, () => work(transaction));
|
|
126
|
+
if (this.version !== baseVersion) {
|
|
127
|
+
throw new Error("Memory transaction conflict: state changed during transaction");
|
|
128
|
+
}
|
|
129
|
+
const committed = cloneTables(transaction.tables);
|
|
130
|
+
this.tables.clear();
|
|
131
|
+
for (const [table, rows] of committed)
|
|
132
|
+
this.tables.set(table, rows);
|
|
133
|
+
this.version += 1;
|
|
134
|
+
return result;
|
|
135
|
+
} finally {
|
|
136
|
+
transaction.close();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
class SnapshotMemoryDatabase {
|
|
142
|
+
tables;
|
|
143
|
+
closed = false;
|
|
144
|
+
constructor(tables) {
|
|
145
|
+
this.tables = tables;
|
|
146
|
+
}
|
|
147
|
+
close() {
|
|
148
|
+
this.closed = true;
|
|
149
|
+
}
|
|
150
|
+
assertOpen() {
|
|
151
|
+
if (this.closed)
|
|
152
|
+
throw new Error("Memory transaction is closed");
|
|
153
|
+
}
|
|
154
|
+
get(table, key) {
|
|
155
|
+
this.assertOpen();
|
|
156
|
+
const value = this.tables.get(table)?.get(key);
|
|
157
|
+
return value === undefined ? undefined : cloneValue(value);
|
|
158
|
+
}
|
|
159
|
+
set(table, key, value) {
|
|
160
|
+
this.assertOpen();
|
|
161
|
+
const rows = this.tables.get(table) ?? new Map;
|
|
162
|
+
rows.set(key, cloneValue(value));
|
|
163
|
+
this.tables.set(table, rows);
|
|
164
|
+
}
|
|
165
|
+
delete(table, key) {
|
|
166
|
+
this.assertOpen();
|
|
167
|
+
return this.tables.get(table)?.delete(key) ?? false;
|
|
168
|
+
}
|
|
169
|
+
list(table) {
|
|
170
|
+
this.assertOpen();
|
|
171
|
+
return [...this.tables.get(table)?.entries() ?? []].map(([key, value]) => ({ key, value: cloneValue(value) })).sort((left, right) => left.key.localeCompare(right.key));
|
|
172
|
+
}
|
|
173
|
+
clear() {
|
|
174
|
+
this.assertOpen();
|
|
175
|
+
this.tables.clear();
|
|
176
|
+
}
|
|
177
|
+
transaction() {
|
|
178
|
+
return Promise.reject(new Error("Nested memory transactions are not supported"));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
class DefaultMemoryStorage {
|
|
183
|
+
objects = new Map;
|
|
184
|
+
pendingFailure;
|
|
185
|
+
policy;
|
|
186
|
+
put(bucket, key, body, options = {}) {
|
|
187
|
+
this.check("put", bucket, key);
|
|
188
|
+
this.objects.set(storageKey(bucket, key), {
|
|
189
|
+
bucket,
|
|
190
|
+
key,
|
|
191
|
+
body: typeof body === "string" ? new TextEncoder().encode(body) : new Uint8Array(body),
|
|
192
|
+
contentType: options.contentType,
|
|
193
|
+
metadata: { ...options.metadata ?? {} }
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
get(bucket, key) {
|
|
197
|
+
this.check("get", bucket, key);
|
|
198
|
+
const object = this.objects.get(storageKey(bucket, key));
|
|
199
|
+
return object ? cloneObject(object) : undefined;
|
|
200
|
+
}
|
|
201
|
+
delete(bucket, key) {
|
|
202
|
+
this.check("delete", bucket, key);
|
|
203
|
+
return this.objects.delete(storageKey(bucket, key));
|
|
204
|
+
}
|
|
205
|
+
list(bucket, prefix = "") {
|
|
206
|
+
this.check("list", bucket, prefix);
|
|
207
|
+
return [...this.objects.values()].filter((object) => object.bucket === bucket && object.key.startsWith(prefix)).filter((object) => !this.policy || this.policy("get", object.bucket, object.key)).sort((left, right) => left.key.localeCompare(right.key)).map(cloneObject);
|
|
208
|
+
}
|
|
209
|
+
clear() {
|
|
210
|
+
this.objects.clear();
|
|
211
|
+
this.pendingFailure = undefined;
|
|
212
|
+
}
|
|
213
|
+
failNext(error = new Error("Memory storage failure")) {
|
|
214
|
+
this.pendingFailure = error;
|
|
215
|
+
}
|
|
216
|
+
setPolicy(policy) {
|
|
217
|
+
this.policy = policy;
|
|
218
|
+
}
|
|
219
|
+
check(operation, bucket, key) {
|
|
220
|
+
if (this.pendingFailure) {
|
|
221
|
+
const error = this.pendingFailure;
|
|
222
|
+
this.pendingFailure = undefined;
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
if (this.policy && !this.policy(operation, bucket, key)) {
|
|
226
|
+
throw new ApplicationError("Memory storage access denied", { status: 403, code: "MEMORY_STORAGE_DENIED" });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function createMemorySandbox(options = {}) {
|
|
231
|
+
const db = new DefaultMemoryDatabase;
|
|
232
|
+
const storage = new DefaultMemoryStorage;
|
|
233
|
+
const policy = createMemoryPolicy();
|
|
234
|
+
const audit = [];
|
|
235
|
+
const governance = {
|
|
236
|
+
authorize(invocation) {
|
|
237
|
+
const subject = policy.subject(invocation.requestContext);
|
|
238
|
+
if (!invocation.command.permission || !policy.can(subject, invocation.command.permission)) {
|
|
239
|
+
throw new ApplicationError("Memory permission denied", { status: 403, code: "MEMORY_PERMISSION_DENIED" });
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
idempotency(invocation, next) {
|
|
243
|
+
return policy.runOnce(policy.subject(invocation.requestContext), invocation.command.name, requireIdempotencyKey(invocation), invocation.input, next);
|
|
244
|
+
},
|
|
245
|
+
transaction(_invocation, next) {
|
|
246
|
+
const count = audit.length;
|
|
247
|
+
return db.transaction(() => next()).catch((error) => {
|
|
248
|
+
audit.splice(count);
|
|
249
|
+
throw error;
|
|
250
|
+
});
|
|
251
|
+
},
|
|
252
|
+
audit: {
|
|
253
|
+
succeeded(invocation) {
|
|
254
|
+
audit.push({ command: invocation.command.name, outcome: "succeeded" });
|
|
255
|
+
},
|
|
256
|
+
failed(invocation) {
|
|
257
|
+
audit.push({ command: invocation.command.name, outcome: "failed" });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
const identity = options.identity ?? { authenticated: false };
|
|
262
|
+
const requestId = options.requestId ?? "memory-request";
|
|
263
|
+
const configuredContext = options.requestContext;
|
|
264
|
+
const app = createApplication({
|
|
265
|
+
...options,
|
|
266
|
+
commandGovernance: options.commandGovernance ?? (options.memoryGovernance ? governance : undefined),
|
|
267
|
+
deps: {
|
|
268
|
+
...options.deps ?? {},
|
|
269
|
+
dbClient: db,
|
|
270
|
+
memoryDb: db,
|
|
271
|
+
storage,
|
|
272
|
+
memoryStorage: storage,
|
|
273
|
+
memoryPolicy: policy
|
|
274
|
+
},
|
|
275
|
+
requestContext: configuredContext ?? ((request) => ({
|
|
276
|
+
requestId: request.headers.get("x-request-id") ?? requestId,
|
|
277
|
+
request,
|
|
278
|
+
identity
|
|
279
|
+
}))
|
|
280
|
+
});
|
|
281
|
+
return {
|
|
282
|
+
app,
|
|
283
|
+
db,
|
|
284
|
+
storage,
|
|
285
|
+
policy,
|
|
286
|
+
audit,
|
|
287
|
+
identity,
|
|
288
|
+
request(path, init = {}) {
|
|
289
|
+
const url = path.startsWith("http://") || path.startsWith("https://") ? path : `http://memory.local${path.startsWith("/") ? path : `/${path}`}`;
|
|
290
|
+
return app.handle(new Request(url, init));
|
|
291
|
+
},
|
|
292
|
+
reset() {
|
|
293
|
+
db.clear();
|
|
294
|
+
storage.clear();
|
|
295
|
+
policy.clear();
|
|
296
|
+
audit.length = 0;
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
function storageKey(bucket, key) {
|
|
301
|
+
return JSON.stringify([bucket, key]);
|
|
302
|
+
}
|
|
303
|
+
function cloneObject(object) {
|
|
304
|
+
return {
|
|
305
|
+
...object,
|
|
306
|
+
body: new Uint8Array(object.body),
|
|
307
|
+
metadata: { ...object.metadata }
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function cloneTables(tables) {
|
|
311
|
+
return new Map([...tables.entries()].map(([table, rows]) => [
|
|
312
|
+
table,
|
|
313
|
+
new Map([...rows.entries()].map(([key, value]) => [key, cloneValue(value)]))
|
|
314
|
+
]));
|
|
315
|
+
}
|
|
316
|
+
function cloneValue(value) {
|
|
317
|
+
return structuredClone(value);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/index.ts
|
|
3
321
|
var VERIFIED_JWT_SUBJECT_HEADER = "x-supacloud-jwt-sub";
|
|
4
322
|
var EXECUTION_ID_HEADER = "x-sb-execution-id";
|
|
5
323
|
var IDEMPOTENCY_KEY_HEADER = "idempotency-key";
|
|
@@ -250,11 +568,13 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
|
|
|
250
568
|
body: ctx.body,
|
|
251
569
|
params: ctx.params,
|
|
252
570
|
query: ctx.query,
|
|
571
|
+
headers: Object.fromEntries(ctx.request.headers.entries()),
|
|
572
|
+
context: requestContext,
|
|
253
573
|
request: ctx.request,
|
|
254
574
|
scope: requestScope,
|
|
255
575
|
requestContext
|
|
256
576
|
};
|
|
257
|
-
const invoke = () => Reflect.apply(method, instance, [input]);
|
|
577
|
+
const invoke = () => route.invoker ? route.invoker(instance, input) : Reflect.apply(method, instance, [input]);
|
|
258
578
|
const routeContext = {
|
|
259
579
|
kind: "route",
|
|
260
580
|
name: `${route.method} ${path}`,
|
|
@@ -417,7 +737,7 @@ function createApplication(options) {
|
|
|
417
737
|
return pending;
|
|
418
738
|
};
|
|
419
739
|
const imported = {};
|
|
420
|
-
for (const module of options.modules) {
|
|
740
|
+
for (const module of options.modules ?? []) {
|
|
421
741
|
const services = module.createServices(options.deps ?? {}, imported);
|
|
422
742
|
imported[module.name] = services;
|
|
423
743
|
app.use(createModulePlugin(module, services, ctxFactory, {
|
|
@@ -440,6 +760,8 @@ export {
|
|
|
440
760
|
composeCommandExecutors,
|
|
441
761
|
createApplication,
|
|
442
762
|
createCommandExecutor,
|
|
763
|
+
createMemoryPolicy,
|
|
764
|
+
createMemorySandbox,
|
|
443
765
|
createModulePlugin,
|
|
444
766
|
createSupaCloudRequestContext,
|
|
445
767
|
createTestApp,
|
package/dist/memory.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { type ApplicationOptions, type TrustedRequestIdentity } from "./index";
|
|
2
|
+
import { type MemoryPolicy } from "./memory_policy";
|
|
3
|
+
export interface HandleLike {
|
|
4
|
+
handle(request: Request): Promise<Response> | Response;
|
|
5
|
+
}
|
|
6
|
+
export interface MemoryDatabase {
|
|
7
|
+
get(table: string, key: string): unknown;
|
|
8
|
+
set(table: string, key: string, value: unknown): void;
|
|
9
|
+
delete(table: string, key: string): boolean;
|
|
10
|
+
list(table: string): Array<{
|
|
11
|
+
key: string;
|
|
12
|
+
value: unknown;
|
|
13
|
+
}>;
|
|
14
|
+
clear(): void;
|
|
15
|
+
transaction<T>(work: (tx: MemoryDatabase) => T | Promise<T>): Promise<T>;
|
|
16
|
+
}
|
|
17
|
+
export interface MemoryStorageObject {
|
|
18
|
+
bucket: string;
|
|
19
|
+
key: string;
|
|
20
|
+
body: Uint8Array;
|
|
21
|
+
contentType?: string;
|
|
22
|
+
metadata: Record<string, string>;
|
|
23
|
+
}
|
|
24
|
+
export interface MemoryStorage {
|
|
25
|
+
put(bucket: string, key: string, body: Uint8Array | string, options?: {
|
|
26
|
+
contentType?: string;
|
|
27
|
+
metadata?: Record<string, string>;
|
|
28
|
+
}): void;
|
|
29
|
+
get(bucket: string, key: string): MemoryStorageObject | undefined;
|
|
30
|
+
delete(bucket: string, key: string): boolean;
|
|
31
|
+
list(bucket: string, prefix?: string): MemoryStorageObject[];
|
|
32
|
+
clear(): void;
|
|
33
|
+
failNext(error?: Error): void;
|
|
34
|
+
setPolicy(policy?: (operation: "put" | "get" | "delete" | "list", bucket: string, key: string) => boolean): void;
|
|
35
|
+
}
|
|
36
|
+
export interface MemorySandboxOptions extends Omit<ApplicationOptions, "deps" | "requestContext"> {
|
|
37
|
+
/** Stable identity used by every request unless overridden by requestContext. */
|
|
38
|
+
identity?: TrustedRequestIdentity;
|
|
39
|
+
/** Stable request id used when the request does not provide x-request-id. */
|
|
40
|
+
requestId?: string;
|
|
41
|
+
/** Extend the platform dependency bag without replacing memory dependencies. */
|
|
42
|
+
deps?: Record<string, unknown>;
|
|
43
|
+
/** Replace the default deterministic request context factory. */
|
|
44
|
+
requestContext?: ApplicationOptions["requestContext"];
|
|
45
|
+
/** Enable the test-only permission, receipt, transaction and audit adapters. */
|
|
46
|
+
memoryGovernance?: boolean;
|
|
47
|
+
}
|
|
48
|
+
export interface MemorySandbox {
|
|
49
|
+
app: HandleLike;
|
|
50
|
+
db: MemoryDatabase;
|
|
51
|
+
storage: MemoryStorage;
|
|
52
|
+
policy: MemoryPolicy;
|
|
53
|
+
audit: Array<{
|
|
54
|
+
command: string;
|
|
55
|
+
outcome: "succeeded" | "failed";
|
|
56
|
+
}>;
|
|
57
|
+
identity: TrustedRequestIdentity;
|
|
58
|
+
request(path: string, init?: RequestInit): Promise<Response>;
|
|
59
|
+
reset(): void;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Creates a deterministic in-process application harness.
|
|
63
|
+
*
|
|
64
|
+
* The database and storage are intentionally small adapter contracts, not
|
|
65
|
+
* PostgreSQL or S3 emulators. They are sufficient for fast command and HTTP
|
|
66
|
+
* behavior tests while keeping production adapters replaceable.
|
|
67
|
+
*/
|
|
68
|
+
export declare function createMemorySandbox(options?: MemorySandboxOptions): MemorySandbox;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface MemoryPolicy {
|
|
2
|
+
grant(subject: string, permission: string): void;
|
|
3
|
+
revoke(subject: string, permission: string): void;
|
|
4
|
+
can(subject: string, permission: string): boolean;
|
|
5
|
+
claimIdempotency(subject: string, key: string): boolean;
|
|
6
|
+
subject(context: unknown): string;
|
|
7
|
+
runOnce<T>(subject: string, command: string, key: string, input: unknown, work: () => T | Promise<T>): Promise<T>;
|
|
8
|
+
clear(): void;
|
|
9
|
+
}
|
|
10
|
+
/** Test-only grants and successful-result receipts, scoped by subject and command. */
|
|
11
|
+
export declare function createMemoryPolicy(): MemoryPolicy;
|
package/package.json
CHANGED