@human-synthesis/norns 0.0.15 → 0.1.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/bin/norns.js +146 -6
- package/package.json +9 -3
- package/src/auto-import.js +7 -3
- package/src/config.js +17 -3
- package/src/kernel/absorb.js +279 -0
- package/src/kernel/address.js +224 -0
- package/src/kernel/adopt.js +157 -0
- package/src/kernel/emit-machines.js +87 -0
- package/src/kernel/emit-schema.js +199 -0
- package/src/kernel/emit-units.js +587 -0
- package/src/kernel/emit-wrangler.js +114 -0
- package/src/kernel/expr-compile.js +188 -0
- package/src/kernel/expr.js +290 -0
- package/src/kernel/generate.js +397 -0
- package/src/kernel/graph.js +222 -0
- package/src/kernel/index.js +71 -0
- package/src/kernel/meta.js +237 -0
- package/src/kernel/migrate.js +134 -0
- package/src/kernel/refine.js +199 -0
- package/src/kernel/trace.js +277 -0
- package/src/kernel/validate.js +92 -0
- package/src/live-client.js +72 -0
- package/src/server/boot.js +67 -4
- package/src/server/cron.js +105 -0
- package/src/server/db.js +61 -0
- package/src/server/events.js +86 -0
- package/src/server/guard.js +48 -0
- package/src/server/handle/auth.js +54 -0
- package/src/server/index.js +13 -2
- package/src/server/live.js +134 -0
- package/src/server/machine.js +35 -0
- package/src/server/page.js +7 -3
- package/src/server/room.js +162 -0
- package/src/server/route.js +46 -2
- package/src/server/storage.js +97 -0
package/src/server/route.js
CHANGED
|
@@ -13,13 +13,44 @@ import { validate, ValidationError } from './validate.js';
|
|
|
13
13
|
* @property {any} user shortcut for `event.locals.user`
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {Object} Serializer
|
|
18
|
+
* @property {(result: any, event: RequestEvent) => Response | null} serialize
|
|
19
|
+
* turn the handler's return value into a Response, or return null to fall
|
|
20
|
+
* through to the default JSON serialization
|
|
21
|
+
* @property {(request: Request, contentType: string) => Promise<any> | undefined} [parseBody]
|
|
22
|
+
* read a request body for a content type route() doesn't handle natively;
|
|
23
|
+
* return undefined to fall through to the built-in JSON/form readers
|
|
24
|
+
*/
|
|
25
|
+
|
|
16
26
|
/**
|
|
17
27
|
* @typedef {Object} RouteOptions
|
|
18
28
|
* @property {any} [input] body schema (Standard Schema or function)
|
|
19
29
|
* @property {any} [query] query schema (Standard Schema or function)
|
|
30
|
+
* @property {Serializer | null} [serializer] per-route serializer; null forces
|
|
31
|
+
* plain JSON even when an app-wide serializer is set
|
|
20
32
|
* @property {(ctx: RouteContext) => any | Promise<any>} handler
|
|
21
33
|
*/
|
|
22
34
|
|
|
35
|
+
/** @type {Serializer | null} */
|
|
36
|
+
let defaultSerializer = null;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Set the app-wide response serializer used by every route() that doesn't
|
|
40
|
+
* declare its own (e.g. tronSerializer() from @human-synthesis/norns-tron).
|
|
41
|
+
* Pass null to go back to plain JSON. Usually wired via boot({ serializer }).
|
|
42
|
+
*
|
|
43
|
+
* @param {Serializer | null} serializer
|
|
44
|
+
*/
|
|
45
|
+
export function setSerializer(serializer) {
|
|
46
|
+
defaultSerializer = serializer ?? null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** @returns {Serializer | null} */
|
|
50
|
+
export function getSerializer() {
|
|
51
|
+
return defaultSerializer;
|
|
52
|
+
}
|
|
53
|
+
|
|
23
54
|
/**
|
|
24
55
|
* Wrap a `+server.c` handler. Bakes in:
|
|
25
56
|
* 1. body parsing (JSON / urlencoded / multipart) + validation
|
|
@@ -39,13 +70,17 @@ export function route(opts) {
|
|
|
39
70
|
if (typeof handler !== 'function') {
|
|
40
71
|
throw new Error('route(): `handler` is required');
|
|
41
72
|
}
|
|
73
|
+
const hasOwnSerializer = 'serializer' in opts;
|
|
42
74
|
|
|
43
75
|
return async (event) => {
|
|
44
76
|
const container = event.locals.container;
|
|
77
|
+
// Resolved per request so boot({ serializer }) applies regardless of
|
|
78
|
+
// module evaluation order.
|
|
79
|
+
const serializer = hasOwnSerializer ? opts.serializer : defaultSerializer;
|
|
45
80
|
|
|
46
81
|
let input;
|
|
47
82
|
if (inputSchema !== undefined) {
|
|
48
|
-
const raw = await readBody(event.request);
|
|
83
|
+
const raw = await readBody(event.request, serializer);
|
|
49
84
|
try {
|
|
50
85
|
input = validate(inputSchema, raw);
|
|
51
86
|
} catch (e) {
|
|
@@ -78,6 +113,10 @@ export function route(opts) {
|
|
|
78
113
|
});
|
|
79
114
|
|
|
80
115
|
if (result instanceof Response) return result;
|
|
116
|
+
if (serializer?.serialize) {
|
|
117
|
+
const response = serializer.serialize(result ?? null, event);
|
|
118
|
+
if (response instanceof Response) return response;
|
|
119
|
+
}
|
|
81
120
|
return json(result ?? null);
|
|
82
121
|
};
|
|
83
122
|
}
|
|
@@ -88,10 +127,15 @@ export function route(opts) {
|
|
|
88
127
|
* (or accept `null`).
|
|
89
128
|
*
|
|
90
129
|
* @param {Request} request
|
|
130
|
+
* @param {Serializer | null} [serializer]
|
|
91
131
|
* @returns {Promise<any>}
|
|
92
132
|
*/
|
|
93
|
-
async function readBody(request) {
|
|
133
|
+
async function readBody(request, serializer) {
|
|
94
134
|
const contentType = request.headers.get('content-type')?.split(';', 1)[0]?.trim() ?? '';
|
|
135
|
+
if (serializer?.parseBody) {
|
|
136
|
+
const parsed = serializer.parseBody(request, contentType);
|
|
137
|
+
if (parsed !== undefined) return await parsed;
|
|
138
|
+
}
|
|
95
139
|
if (contentType === 'application/json') {
|
|
96
140
|
try {
|
|
97
141
|
return await request.json();
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync, existsSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, normalize, sep } from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Storage behind `container.resolve('storage')` — backing for the `file`
|
|
6
|
+
* field type. Two adapters with one surface:
|
|
7
|
+
*
|
|
8
|
+
* put(key, data, { contentType? }) → { key }
|
|
9
|
+
* get(key) → { body: Uint8Array, contentType? } | null
|
|
10
|
+
* delete(key) → void
|
|
11
|
+
* list(prefix) → string[] (keys, sorted)
|
|
12
|
+
*
|
|
13
|
+
* Keys are `/`-separated paths (`orders/abc/invoice.pdf`).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Cloudflare R2 adapter over a bucket binding.
|
|
18
|
+
* @param {*} bucket R2Bucket binding
|
|
19
|
+
*/
|
|
20
|
+
export function r2Storage(bucket) {
|
|
21
|
+
return {
|
|
22
|
+
async put(key, data, { contentType } = {}) {
|
|
23
|
+
await bucket.put(key, data, contentType ? { httpMetadata: { contentType } } : undefined);
|
|
24
|
+
return { key };
|
|
25
|
+
},
|
|
26
|
+
async get(key) {
|
|
27
|
+
const obj = await bucket.get(key);
|
|
28
|
+
if (!obj) return null;
|
|
29
|
+
return {
|
|
30
|
+
body: new Uint8Array(await obj.arrayBuffer()),
|
|
31
|
+
contentType: obj.httpMetadata?.contentType
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
async delete(key) {
|
|
35
|
+
await bucket.delete(key);
|
|
36
|
+
},
|
|
37
|
+
async list(prefix = '') {
|
|
38
|
+
const keys = [];
|
|
39
|
+
let cursor;
|
|
40
|
+
do {
|
|
41
|
+
const page = await bucket.list({ prefix, cursor });
|
|
42
|
+
for (const obj of page.objects) keys.push(obj.key);
|
|
43
|
+
cursor = page.truncated ? page.cursor : undefined;
|
|
44
|
+
} while (cursor);
|
|
45
|
+
return keys.sort();
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Local-dir shim for `norns dev` / tests. Content types ride in a `.meta`
|
|
52
|
+
* sidecar next to each object.
|
|
53
|
+
* @param {string} root
|
|
54
|
+
*/
|
|
55
|
+
export function dirStorage(root) {
|
|
56
|
+
const safe = (key) => {
|
|
57
|
+
const p = normalize(join(root, key));
|
|
58
|
+
if (!p.startsWith(normalize(root) + sep)) throw new Error(`storage: invalid key ${key}`);
|
|
59
|
+
return p;
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
async put(key, data, { contentType } = {}) {
|
|
63
|
+
const path = safe(key);
|
|
64
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
65
|
+
writeFileSync(path, typeof data === 'string' ? data : new Uint8Array(data));
|
|
66
|
+
if (contentType) writeFileSync(`${path}.meta`, contentType);
|
|
67
|
+
return { key };
|
|
68
|
+
},
|
|
69
|
+
async get(key) {
|
|
70
|
+
const path = safe(key);
|
|
71
|
+
if (!existsSync(path)) return null;
|
|
72
|
+
const meta = existsSync(`${path}.meta`) ? readFileSync(`${path}.meta`, 'utf8') : undefined;
|
|
73
|
+
return { body: new Uint8Array(readFileSync(path)), contentType: meta };
|
|
74
|
+
},
|
|
75
|
+
async delete(key) {
|
|
76
|
+
const path = safe(key);
|
|
77
|
+
rmSync(path, { force: true });
|
|
78
|
+
rmSync(`${path}.meta`, { force: true });
|
|
79
|
+
},
|
|
80
|
+
async list(prefix = '') {
|
|
81
|
+
if (!existsSync(root)) return [];
|
|
82
|
+
const out = [];
|
|
83
|
+
const walk = (dir) => {
|
|
84
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
85
|
+
const full = join(dir, entry.name);
|
|
86
|
+
if (entry.isDirectory()) walk(full);
|
|
87
|
+
else if (!entry.name.endsWith('.meta')) {
|
|
88
|
+
const key = full.slice(normalize(root).length + 1).split(sep).join('/');
|
|
89
|
+
if (key.startsWith(prefix)) out.push(key);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
walk(root);
|
|
94
|
+
return out.sort();
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|