spacetimedb 2.3.0 → 2.4.1
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/LICENSE.txt +2 -2
- package/dist/index.browser.mjs +64 -4
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.cjs +64 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +64 -4
- package/dist/index.mjs.map +1 -1
- package/dist/lib/autogen/types.d.ts +675 -2
- package/dist/lib/autogen/types.d.ts.map +1 -1
- package/dist/lib/schema.d.ts.map +1 -1
- package/dist/min/index.browser.mjs +1 -1
- package/dist/min/index.browser.mjs.map +1 -1
- package/dist/min/sdk/index.browser.mjs +1 -1
- package/dist/min/sdk/index.browser.mjs.map +1 -1
- package/dist/sdk/decompress.d.ts.map +1 -1
- package/dist/sdk/index.browser.mjs +64 -4
- package/dist/sdk/index.browser.mjs.map +1 -1
- package/dist/sdk/index.cjs +64 -4
- package/dist/sdk/index.cjs.map +1 -1
- package/dist/sdk/index.mjs +64 -4
- package/dist/sdk/index.mjs.map +1 -1
- package/dist/server/http.d.ts +1 -2
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.test-d.d.ts +2 -0
- package/dist/server/http.test-d.d.ts.map +1 -0
- package/dist/server/http_handlers.d.ts +82 -0
- package/dist/server/http_handlers.d.ts.map +1 -0
- package/dist/server/http_internal.d.ts +1 -32
- package/dist/server/http_internal.d.ts.map +1 -1
- package/dist/server/http_shared.d.ts +44 -0
- package/dist/server/http_shared.d.ts.map +1 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.mjs +628 -136
- package/dist/server/index.mjs.map +1 -1
- package/dist/server/runtime.d.ts +1 -0
- package/dist/server/runtime.d.ts.map +1 -1
- package/dist/server/schema.d.ts +19 -4
- package/dist/server/schema.d.ts.map +1 -1
- package/dist/server/views.d.ts +17 -1
- package/dist/server/views.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/lib/autogen/types.ts +38 -0
- package/src/lib/schema.ts +21 -0
- package/src/sdk/decompress.ts +19 -4
- package/src/sdk/logger.ts +1 -1
- package/src/server/http.test-d.ts +80 -0
- package/src/server/http.ts +14 -2
- package/src/server/http_handlers.ts +413 -0
- package/src/server/http_internal.ts +15 -142
- package/src/server/http_shared.ts +186 -0
- package/src/server/index.ts +11 -0
- package/src/server/procedures.ts +8 -30
- package/src/server/runtime.ts +137 -1
- package/src/server/schema.ts +86 -4
- package/src/server/sys.d.ts +7 -0
- package/src/server/view.test-d.ts +22 -0
- package/src/server/views.ts +127 -0
- package/dist/lib/http_types.d.ts +0 -2
- package/dist/lib/http_types.d.ts.map +0 -1
- package/src/lib/http_types.ts +0 -8
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { Headers, headersToList } from 'headers-polyfill';
|
|
2
|
+
import type {
|
|
3
|
+
HttpHeaders,
|
|
4
|
+
HttpMethod,
|
|
5
|
+
HttpVersion,
|
|
6
|
+
} from '../lib/autogen/types';
|
|
7
|
+
|
|
8
|
+
export { Headers };
|
|
9
|
+
|
|
10
|
+
export type BodyInit = ArrayBuffer | ArrayBufferView | string;
|
|
11
|
+
export type HeadersInit = [string, string][] | Record<string, string> | Headers;
|
|
12
|
+
|
|
13
|
+
export const textEncoder = new TextEncoder();
|
|
14
|
+
export const textDecoder = new TextDecoder('utf-8');
|
|
15
|
+
|
|
16
|
+
export function deserializeMethod(method: HttpMethod): string {
|
|
17
|
+
switch (method.tag) {
|
|
18
|
+
case 'Get':
|
|
19
|
+
return 'GET';
|
|
20
|
+
case 'Head':
|
|
21
|
+
return 'HEAD';
|
|
22
|
+
case 'Post':
|
|
23
|
+
return 'POST';
|
|
24
|
+
case 'Put':
|
|
25
|
+
return 'PUT';
|
|
26
|
+
case 'Delete':
|
|
27
|
+
return 'DELETE';
|
|
28
|
+
case 'Connect':
|
|
29
|
+
return 'CONNECT';
|
|
30
|
+
case 'Options':
|
|
31
|
+
return 'OPTIONS';
|
|
32
|
+
case 'Trace':
|
|
33
|
+
return 'TRACE';
|
|
34
|
+
case 'Patch':
|
|
35
|
+
return 'PATCH';
|
|
36
|
+
case 'Extension':
|
|
37
|
+
return method.value;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const methods = new Map<string, HttpMethod>([
|
|
42
|
+
['GET', { tag: 'Get' }],
|
|
43
|
+
['HEAD', { tag: 'Head' }],
|
|
44
|
+
['POST', { tag: 'Post' }],
|
|
45
|
+
['PUT', { tag: 'Put' }],
|
|
46
|
+
['DELETE', { tag: 'Delete' }],
|
|
47
|
+
['CONNECT', { tag: 'Connect' }],
|
|
48
|
+
['OPTIONS', { tag: 'Options' }],
|
|
49
|
+
['TRACE', { tag: 'Trace' }],
|
|
50
|
+
['PATCH', { tag: 'Patch' }],
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
export function serializeMethod(method?: string): HttpMethod {
|
|
54
|
+
return (
|
|
55
|
+
methods.get(method?.toUpperCase() ?? 'GET') ?? {
|
|
56
|
+
tag: 'Extension',
|
|
57
|
+
value: method!,
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function serializeHeaders(headers: Headers): HttpHeaders {
|
|
63
|
+
return {
|
|
64
|
+
entries: headersToList(headers as any)
|
|
65
|
+
.flatMap(([k, v]) => (Array.isArray(v) ? v.map(v => [k, v]) : [[k, v]]))
|
|
66
|
+
.map(([name, value]) => ({ name, value: textEncoder.encode(value) })),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function deserializeHeaders(headers: HttpHeaders): Headers {
|
|
71
|
+
return new Headers(
|
|
72
|
+
headers.entries.map(({ name, value }): [string, string] => [
|
|
73
|
+
name,
|
|
74
|
+
textDecoder.decode(value),
|
|
75
|
+
])
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface ResponseInit {
|
|
80
|
+
headers?: HeadersInit;
|
|
81
|
+
status?: number;
|
|
82
|
+
statusText?: string;
|
|
83
|
+
version?: HttpVersion;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface InnerResponse {
|
|
87
|
+
type: 'basic' | 'cors' | 'default' | 'error' | 'opaque' | 'opaqueredirect';
|
|
88
|
+
url: string | null;
|
|
89
|
+
status: number;
|
|
90
|
+
statusText: string;
|
|
91
|
+
headers: Headers;
|
|
92
|
+
aborted: boolean;
|
|
93
|
+
version: HttpVersion;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const makeResponse = Symbol('makeResponse');
|
|
97
|
+
|
|
98
|
+
export class SyncResponse {
|
|
99
|
+
#body: string | ArrayBuffer | null;
|
|
100
|
+
#inner: InnerResponse;
|
|
101
|
+
|
|
102
|
+
constructor(body?: BodyInit | null, init?: ResponseInit) {
|
|
103
|
+
if (body == null) {
|
|
104
|
+
this.#body = null;
|
|
105
|
+
} else if (typeof body === 'string') {
|
|
106
|
+
this.#body = body;
|
|
107
|
+
} else {
|
|
108
|
+
// this call is fine, the typings are just weird
|
|
109
|
+
this.#body = new Uint8Array<ArrayBuffer>(body as any).buffer;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// there's a type mismatch - headers-polyfill's typing doesn't expect its
|
|
113
|
+
// own `Headers` type, even though the actual code handles it correctly.
|
|
114
|
+
this.#inner = {
|
|
115
|
+
headers: new Headers(init?.headers as any),
|
|
116
|
+
status: init?.status ?? 200,
|
|
117
|
+
statusText: init?.statusText ?? '',
|
|
118
|
+
type: 'default',
|
|
119
|
+
url: null,
|
|
120
|
+
aborted: false,
|
|
121
|
+
version: init?.version ?? { tag: 'Http11' },
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
static [makeResponse](body: BodyInit | null, inner: InnerResponse) {
|
|
126
|
+
const me = new SyncResponse(body);
|
|
127
|
+
me.#inner = inner;
|
|
128
|
+
return me;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
get headers(): Headers {
|
|
132
|
+
return this.#inner.headers;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
get status(): number {
|
|
136
|
+
return this.#inner.status;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
get statusText() {
|
|
140
|
+
return this.#inner.statusText;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
get ok(): boolean {
|
|
144
|
+
return 200 <= this.#inner.status && this.#inner.status <= 299;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
get url(): string {
|
|
148
|
+
return this.#inner.url ?? '';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
get type() {
|
|
152
|
+
return this.#inner.type;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
get version(): HttpVersion {
|
|
156
|
+
return this.#inner.version;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
arrayBuffer(): ArrayBuffer {
|
|
160
|
+
return this.bytes().buffer;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
bytes(): Uint8Array<ArrayBuffer> {
|
|
164
|
+
if (this.#body == null) {
|
|
165
|
+
return new Uint8Array();
|
|
166
|
+
} else if (typeof this.#body === 'string') {
|
|
167
|
+
return textEncoder.encode(this.#body);
|
|
168
|
+
} else {
|
|
169
|
+
return new Uint8Array(this.#body);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
json(): any {
|
|
174
|
+
return JSON.parse(this.text());
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
text(): string {
|
|
178
|
+
if (this.#body == null) {
|
|
179
|
+
return '';
|
|
180
|
+
} else if (typeof this.#body === 'string') {
|
|
181
|
+
return this.#body;
|
|
182
|
+
} else {
|
|
183
|
+
return textDecoder.decode(this.#body);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
package/src/server/index.ts
CHANGED
|
@@ -22,5 +22,16 @@ export type { Uuid } from '../lib/uuid';
|
|
|
22
22
|
export type { Random } from './rng';
|
|
23
23
|
export type { ViewExport, ViewCtx, AnonymousViewCtx } from './views';
|
|
24
24
|
export { Range, type Bound } from './range';
|
|
25
|
+
export {
|
|
26
|
+
Headers,
|
|
27
|
+
Request,
|
|
28
|
+
SyncResponse,
|
|
29
|
+
Router,
|
|
30
|
+
type BodyInit,
|
|
31
|
+
type HeadersInit,
|
|
32
|
+
type RequestInit,
|
|
33
|
+
type ResponseInit,
|
|
34
|
+
} from './http';
|
|
35
|
+
export type { HandlerContext, HttpHandlerExport } from './http';
|
|
25
36
|
|
|
26
37
|
import './polyfills'; // Ensure polyfills are loaded
|
package/src/server/procedures.ts
CHANGED
|
@@ -22,7 +22,7 @@ import { Uuid } from '../lib/uuid';
|
|
|
22
22
|
import { httpClient, type HttpClient } from './http_internal';
|
|
23
23
|
import type { DbView } from './db_view';
|
|
24
24
|
import { makeRandom, type Random } from './rng';
|
|
25
|
-
import { callUserFunction, ReducerCtxImpl, sys } from './runtime';
|
|
25
|
+
import { callUserFunction, ReducerCtxImpl, runWithTx, sys } from './runtime';
|
|
26
26
|
import {
|
|
27
27
|
exportContext,
|
|
28
28
|
registerExport,
|
|
@@ -214,38 +214,16 @@ const ProcedureCtxImpl = class ProcedureCtx<S extends UntypedSchemaDef>
|
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
withTx<T>(body: (ctx: TransactionCtx<S>) => T): T {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
try {
|
|
221
|
-
const ctx: TransactionCtx<S> = new TransactionCtxImpl(
|
|
217
|
+
return runWithTx(
|
|
218
|
+
timestamp =>
|
|
219
|
+
new TransactionCtxImpl(
|
|
222
220
|
this.sender,
|
|
223
|
-
|
|
221
|
+
timestamp,
|
|
224
222
|
this.connectionId,
|
|
225
223
|
this.#dbView()
|
|
226
|
-
)
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
sys.procedure_abort_mut_tx();
|
|
230
|
-
throw e;
|
|
231
|
-
}
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
let res = run();
|
|
235
|
-
try {
|
|
236
|
-
sys.procedure_commit_mut_tx();
|
|
237
|
-
return res;
|
|
238
|
-
} catch {
|
|
239
|
-
// ignore the commit error
|
|
240
|
-
}
|
|
241
|
-
console.warn('committing anonymous transaction failed');
|
|
242
|
-
res = run();
|
|
243
|
-
try {
|
|
244
|
-
sys.procedure_commit_mut_tx();
|
|
245
|
-
return res;
|
|
246
|
-
} catch (e) {
|
|
247
|
-
throw new Error('transaction retry failed again', { cause: e });
|
|
248
|
-
}
|
|
224
|
+
) as TransactionCtx<S>,
|
|
225
|
+
body
|
|
226
|
+
);
|
|
249
227
|
}
|
|
250
228
|
|
|
251
229
|
newUuidV4(): Uuid {
|
package/src/server/runtime.ts
CHANGED
|
@@ -27,6 +27,18 @@ import {
|
|
|
27
27
|
type UniqueIndex,
|
|
28
28
|
} from '../lib/indexes';
|
|
29
29
|
import { callProcedure } from './procedures';
|
|
30
|
+
import {
|
|
31
|
+
type HandlerContext,
|
|
32
|
+
Request,
|
|
33
|
+
SyncResponse,
|
|
34
|
+
makeRequest,
|
|
35
|
+
} from './http_handlers';
|
|
36
|
+
import { httpClient } from './http_internal';
|
|
37
|
+
import {
|
|
38
|
+
deserializeHeaders,
|
|
39
|
+
deserializeMethod,
|
|
40
|
+
serializeHeaders,
|
|
41
|
+
} from './http_shared';
|
|
30
42
|
import {
|
|
31
43
|
type AuthCtx,
|
|
32
44
|
type JsonObject,
|
|
@@ -35,7 +47,7 @@ import {
|
|
|
35
47
|
} from '../lib/reducers';
|
|
36
48
|
import { type UntypedSchemaDef } from '../lib/schema';
|
|
37
49
|
import { type RowType, type Table, type TableMethods } from '../lib/table';
|
|
38
|
-
import { hasOwn } from '../lib/util';
|
|
50
|
+
import { bsatnBaseSize, hasOwn } from '../lib/util';
|
|
39
51
|
import { type AnonymousViewCtx, type ViewCtx } from './views';
|
|
40
52
|
import { isRowTypedQuery, makeQueryBuilder, toSql } from './query';
|
|
41
53
|
import type { DbView } from './db_view';
|
|
@@ -43,11 +55,32 @@ import { getErrorConstructor, SenderError } from './errors';
|
|
|
43
55
|
import { Range, type Bound } from './range';
|
|
44
56
|
import { makeRandom, type Random } from './rng';
|
|
45
57
|
import type { SchemaInner } from './schema';
|
|
58
|
+
import { HttpRequest, HttpResponse } from '../lib/autogen/types';
|
|
46
59
|
|
|
47
60
|
const { freeze } = Object;
|
|
48
61
|
|
|
49
62
|
export const sys = { ..._syscalls2_0, ..._syscalls2_1 };
|
|
50
63
|
|
|
64
|
+
function requestFromWire(request: HttpRequest, body: Uint8Array): Request {
|
|
65
|
+
return Request[makeRequest](body, {
|
|
66
|
+
headers: deserializeHeaders(request.headers),
|
|
67
|
+
method: deserializeMethod(request.method),
|
|
68
|
+
uri: request.uri,
|
|
69
|
+
version: request.version,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function responseIntoWire(response: SyncResponse): [HttpResponse, Uint8Array] {
|
|
74
|
+
return [
|
|
75
|
+
{
|
|
76
|
+
headers: serializeHeaders(response.headers),
|
|
77
|
+
version: response.version,
|
|
78
|
+
code: response.status,
|
|
79
|
+
},
|
|
80
|
+
response.bytes(),
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
|
|
51
84
|
export function parseJsonObject(json: string): JsonObject {
|
|
52
85
|
let value: unknown;
|
|
53
86
|
|
|
@@ -272,6 +305,38 @@ export const callUserFunction = function __spacetimedb_end_short_backtrace<
|
|
|
272
305
|
return fn(...args);
|
|
273
306
|
};
|
|
274
307
|
|
|
308
|
+
export function runWithTx<T, Ctx>(
|
|
309
|
+
makeCtx: (timestamp: Timestamp) => Ctx,
|
|
310
|
+
body: (ctx: Ctx) => T
|
|
311
|
+
): T {
|
|
312
|
+
const run = () => {
|
|
313
|
+
const timestamp = sys.procedure_start_mut_tx();
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
return body(makeCtx(new Timestamp(timestamp)));
|
|
317
|
+
} catch (e) {
|
|
318
|
+
sys.procedure_abort_mut_tx();
|
|
319
|
+
throw e;
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
let res = run();
|
|
324
|
+
try {
|
|
325
|
+
sys.procedure_commit_mut_tx();
|
|
326
|
+
return res;
|
|
327
|
+
} catch {
|
|
328
|
+
// ignore the commit error
|
|
329
|
+
}
|
|
330
|
+
console.warn('committing anonymous transaction failed');
|
|
331
|
+
res = run();
|
|
332
|
+
try {
|
|
333
|
+
sys.procedure_commit_mut_tx();
|
|
334
|
+
return res;
|
|
335
|
+
} catch (e) {
|
|
336
|
+
throw new Error('transaction retry failed again', { cause: e });
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
275
340
|
export const makeHooks = (schema: SchemaInner): ModuleHooks =>
|
|
276
341
|
new ModuleHooksImpl(schema);
|
|
277
342
|
|
|
@@ -418,11 +483,82 @@ class ModuleHooksImpl implements ModuleHooks {
|
|
|
418
483
|
() => this.#dbView
|
|
419
484
|
);
|
|
420
485
|
}
|
|
486
|
+
|
|
487
|
+
__call_http_handler__(
|
|
488
|
+
id: u32,
|
|
489
|
+
timestamp: bigint,
|
|
490
|
+
request: Uint8Array,
|
|
491
|
+
body: Uint8Array
|
|
492
|
+
): [response: Uint8Array, body: Uint8Array] {
|
|
493
|
+
const moduleCtx = this.#schema;
|
|
494
|
+
const handler = moduleCtx.httpHandlers[id];
|
|
495
|
+
const ctx = new HandlerContextImpl(
|
|
496
|
+
new Timestamp(timestamp),
|
|
497
|
+
() => this.#dbView
|
|
498
|
+
);
|
|
499
|
+
const requestMetadata = HttpRequest.deserialize(new BinaryReader(request));
|
|
500
|
+
const response = callUserFunction(
|
|
501
|
+
handler,
|
|
502
|
+
ctx,
|
|
503
|
+
requestFromWire(requestMetadata, body)
|
|
504
|
+
);
|
|
505
|
+
const [responseMetadata, responseBody] = responseIntoWire(response);
|
|
506
|
+
const responseBuf = new BinaryWriter(
|
|
507
|
+
bsatnBaseSize(moduleCtx.typespace, HttpResponse.algebraicType)
|
|
508
|
+
);
|
|
509
|
+
HttpResponse.serialize(responseBuf, responseMetadata);
|
|
510
|
+
return [responseBuf.getBuffer(), responseBody];
|
|
511
|
+
}
|
|
421
512
|
}
|
|
422
513
|
|
|
423
514
|
const BINARY_WRITER = new BinaryWriter(0);
|
|
424
515
|
const BINARY_READER = new BinaryReader(new Uint8Array());
|
|
425
516
|
|
|
517
|
+
class HandlerContextImpl<S extends UntypedSchemaDef = UntypedSchemaDef>
|
|
518
|
+
implements HandlerContext<S>
|
|
519
|
+
{
|
|
520
|
+
#identity: Identity | undefined;
|
|
521
|
+
#uuidCounter: { value: number } | undefined;
|
|
522
|
+
#random: Random | undefined;
|
|
523
|
+
#dbView: () => DbView<any>;
|
|
524
|
+
|
|
525
|
+
readonly http = httpClient;
|
|
526
|
+
|
|
527
|
+
constructor(
|
|
528
|
+
readonly timestamp: Timestamp,
|
|
529
|
+
dbView: () => DbView<any>
|
|
530
|
+
) {
|
|
531
|
+
this.#dbView = dbView;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
get identity() {
|
|
535
|
+
return (this.#identity ??= new Identity(sys.identity()));
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
get random() {
|
|
539
|
+
return (this.#random ??= makeRandom(this.timestamp));
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
withTx<T>(body: (ctx: any) => T): T {
|
|
543
|
+
return runWithTx(
|
|
544
|
+
timestamp =>
|
|
545
|
+
new ReducerCtxImpl(Identity.zero(), timestamp, null, this.#dbView()),
|
|
546
|
+
body
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
newUuidV4(): Uuid {
|
|
551
|
+
const bytes = this.random.fill(new Uint8Array(16));
|
|
552
|
+
return Uuid.fromRandomBytesV4(bytes);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
newUuidV7(): Uuid {
|
|
556
|
+
const bytes = this.random.fill(new Uint8Array(4));
|
|
557
|
+
const counter = (this.#uuidCounter ??= { value: 0 });
|
|
558
|
+
return Uuid.fromCounterV7(counter, this.timestamp, bytes);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
426
562
|
function makeTableView(
|
|
427
563
|
typespace: Typespace,
|
|
428
564
|
table: RawTableDefV10
|
package/src/server/schema.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { moduleHooks, type ModuleDefaultExport } from 'spacetime:sys@2.0';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
CaseConversionPolicy,
|
|
4
|
+
Lifecycle,
|
|
5
|
+
type MethodOrAny,
|
|
6
|
+
} from '../lib/autogen/types';
|
|
3
7
|
import {
|
|
4
8
|
type ParamsAsObject,
|
|
5
9
|
type ParamsObj,
|
|
@@ -14,6 +18,14 @@ import {
|
|
|
14
18
|
} from '../lib/schema';
|
|
15
19
|
import type { UntypedTableSchema } from '../lib/table_schema';
|
|
16
20
|
import { ColumnBuilder, TypeBuilder } from '../lib/type_builders';
|
|
21
|
+
import {
|
|
22
|
+
Router,
|
|
23
|
+
type HandlerFn,
|
|
24
|
+
type HttpHandlerExport,
|
|
25
|
+
type HttpHandlerOpts,
|
|
26
|
+
makeHttpHandlerExport,
|
|
27
|
+
makeHttpRouterExport,
|
|
28
|
+
} from './http_handlers';
|
|
17
29
|
import {
|
|
18
30
|
makeProcedureExport,
|
|
19
31
|
type ProcedureExport,
|
|
@@ -38,6 +50,7 @@ import {
|
|
|
38
50
|
type ViewFn,
|
|
39
51
|
type ViewOpts,
|
|
40
52
|
type ViewReturnTypeBuilder,
|
|
53
|
+
type ValidateViewPrimaryKey,
|
|
41
54
|
type Views,
|
|
42
55
|
} from './views';
|
|
43
56
|
import type { UntypedTableDef } from '../lib/table';
|
|
@@ -47,10 +60,12 @@ export class SchemaInner<
|
|
|
47
60
|
> extends ModuleContext {
|
|
48
61
|
schemaType: S;
|
|
49
62
|
existingFunctions = new Set<string>();
|
|
63
|
+
existingHttpHandlers = new Set<string>();
|
|
50
64
|
reducers: Reducers = [];
|
|
51
65
|
procedures: Procedures = [];
|
|
52
66
|
views: Views = [];
|
|
53
67
|
anonViews: AnonViews = [];
|
|
68
|
+
httpHandlers: HandlerFn[] = [];
|
|
54
69
|
/**
|
|
55
70
|
* Maps ReducerExport objects to the name of the reducer.
|
|
56
71
|
* Used for resolving the reducers of scheduled tables.
|
|
@@ -60,7 +75,10 @@ export class SchemaInner<
|
|
|
60
75
|
| ProcedureExport<UntypedSchemaDef, any, any>,
|
|
61
76
|
string
|
|
62
77
|
> = new Map();
|
|
78
|
+
httpHandlerExports: Map<HttpHandlerExport<UntypedSchemaDef>, string> =
|
|
79
|
+
new Map();
|
|
63
80
|
pendingSchedules: PendingSchedule[] = [];
|
|
81
|
+
pendingHttpRoutes: PendingHttpRoute[] = [];
|
|
64
82
|
|
|
65
83
|
constructor(getSchemaType: (ctx: SchemaInner<S>) => S) {
|
|
66
84
|
super();
|
|
@@ -70,12 +88,21 @@ export class SchemaInner<
|
|
|
70
88
|
defineFunction(name: string) {
|
|
71
89
|
if (this.existingFunctions.has(name)) {
|
|
72
90
|
throw new TypeError(
|
|
73
|
-
`There is already a reducer or
|
|
91
|
+
`There is already a reducer, procedure, or view with the name '${name}'`
|
|
74
92
|
);
|
|
75
93
|
}
|
|
76
94
|
this.existingFunctions.add(name);
|
|
77
95
|
}
|
|
78
96
|
|
|
97
|
+
defineHttpHandler(name: string) {
|
|
98
|
+
if (this.existingHttpHandlers.has(name)) {
|
|
99
|
+
throw new TypeError(
|
|
100
|
+
`There is already an HTTP handler with the name '${name}'`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
this.existingHttpHandlers.add(name);
|
|
104
|
+
}
|
|
105
|
+
|
|
79
106
|
resolveSchedules() {
|
|
80
107
|
for (const { reducer, scheduleAtCol, tableName } of this.pendingSchedules) {
|
|
81
108
|
const functionName = this.functionExports.get(reducer());
|
|
@@ -91,9 +118,30 @@ export class SchemaInner<
|
|
|
91
118
|
});
|
|
92
119
|
}
|
|
93
120
|
}
|
|
121
|
+
|
|
122
|
+
resolveHttpRoutes() {
|
|
123
|
+
for (const route of this.pendingHttpRoutes) {
|
|
124
|
+
const handlerFunction = this.httpHandlerExports.get(route.handler);
|
|
125
|
+
if (handlerFunction === undefined) {
|
|
126
|
+
throw new TypeError(
|
|
127
|
+
`HTTP route for path '${route.path}' refers to a handler that was not exported.`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
this.moduleDef.httpRoutes.push({
|
|
131
|
+
handlerFunction,
|
|
132
|
+
method: route.method,
|
|
133
|
+
path: route.path,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
94
137
|
}
|
|
95
138
|
|
|
96
139
|
type PendingSchedule = UntypedTableSchema['schedule'] & { tableName: string };
|
|
140
|
+
type PendingHttpRoute = {
|
|
141
|
+
handler: HttpHandlerExport<UntypedSchemaDef>;
|
|
142
|
+
method: MethodOrAny;
|
|
143
|
+
path: string;
|
|
144
|
+
};
|
|
97
145
|
|
|
98
146
|
/**
|
|
99
147
|
* The Schema class represents the database schema for a SpacetimeDB application.
|
|
@@ -153,6 +201,7 @@ export class Schema<S extends UntypedSchemaDef> implements ModuleDefaultExport {
|
|
|
153
201
|
moduleExport[registerExport](registeredSchema, name);
|
|
154
202
|
}
|
|
155
203
|
registeredSchema.resolveSchedules();
|
|
204
|
+
registeredSchema.resolveHttpRoutes();
|
|
156
205
|
return makeHooks(registeredSchema);
|
|
157
206
|
}
|
|
158
207
|
|
|
@@ -347,7 +396,11 @@ export class Schema<S extends UntypedSchemaDef> implements ModuleDefaultExport {
|
|
|
347
396
|
view<Ret extends ViewReturnTypeBuilder, F extends ViewFn<S, {}, Ret>>(
|
|
348
397
|
opts: ViewOpts,
|
|
349
398
|
ret: Ret,
|
|
350
|
-
fn: F
|
|
399
|
+
fn: F,
|
|
400
|
+
// Compile-time-only guard: this rest parameter is `[]` for valid return
|
|
401
|
+
// builders, but becomes a required error tuple when a returned row builder
|
|
402
|
+
// marks more than one column with `.primaryKey()`.
|
|
403
|
+
..._: ValidateViewPrimaryKey<Ret>
|
|
351
404
|
): ViewExport<F> {
|
|
352
405
|
return makeViewExport<S, {}, Ret, F>(this.#ctx, opts, {}, ret, fn);
|
|
353
406
|
}
|
|
@@ -380,7 +433,15 @@ export class Schema<S extends UntypedSchemaDef> implements ModuleDefaultExport {
|
|
|
380
433
|
anonymousView<
|
|
381
434
|
Ret extends ViewReturnTypeBuilder,
|
|
382
435
|
F extends AnonymousViewFn<S, {}, Ret>,
|
|
383
|
-
>(
|
|
436
|
+
>(
|
|
437
|
+
opts: ViewOpts,
|
|
438
|
+
ret: Ret,
|
|
439
|
+
fn: F,
|
|
440
|
+
// Compile-time-only guard: this rest parameter is `[]` for valid return
|
|
441
|
+
// builders, but becomes a required error tuple when a returned row builder
|
|
442
|
+
// marks more than one column with `.primaryKey()`.
|
|
443
|
+
..._: ValidateViewPrimaryKey<Ret>
|
|
444
|
+
): ViewExport<F> {
|
|
384
445
|
return makeAnonViewExport<S, {}, Ret, F>(this.#ctx, opts, {}, ret, fn);
|
|
385
446
|
}
|
|
386
447
|
|
|
@@ -458,6 +519,27 @@ export class Schema<S extends UntypedSchemaDef> implements ModuleDefaultExport {
|
|
|
458
519
|
return makeProcedureExport(this.#ctx, opts, params, ret, fn);
|
|
459
520
|
}
|
|
460
521
|
|
|
522
|
+
httpHandler(fn: HandlerFn<S>): HttpHandlerExport<S>;
|
|
523
|
+
httpHandler(opts: HttpHandlerOpts, fn: HandlerFn<S>): HttpHandlerExport<S>;
|
|
524
|
+
httpHandler(
|
|
525
|
+
...args: [HandlerFn<S>] | [HttpHandlerOpts, HandlerFn<S>]
|
|
526
|
+
): HttpHandlerExport<S> {
|
|
527
|
+
let opts: HttpHandlerOpts | undefined, fn: HandlerFn<S>;
|
|
528
|
+
switch (args.length) {
|
|
529
|
+
case 1:
|
|
530
|
+
[fn] = args;
|
|
531
|
+
break;
|
|
532
|
+
case 2:
|
|
533
|
+
[opts, fn] = args;
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
return makeHttpHandlerExport(this.#ctx, opts, fn);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
httpRouter(router: Router): ModuleExport {
|
|
540
|
+
return makeHttpRouterExport(this.#ctx, router);
|
|
541
|
+
}
|
|
542
|
+
|
|
461
543
|
/**
|
|
462
544
|
* Bundle multiple reducers, procedures, etc into one value to export.
|
|
463
545
|
* The name they will be exported with is their corresponding key in the `exports` argument.
|
package/src/server/sys.d.ts
CHANGED
|
@@ -37,6 +37,13 @@ declare module 'spacetime:sys@2.0' {
|
|
|
37
37
|
timestamp: bigint,
|
|
38
38
|
args: Uint8Array
|
|
39
39
|
): Uint8Array;
|
|
40
|
+
|
|
41
|
+
__call_http_handler__(
|
|
42
|
+
id: u32,
|
|
43
|
+
timestamp: bigint,
|
|
44
|
+
request: Uint8Array,
|
|
45
|
+
body: Uint8Array
|
|
46
|
+
): [response: Uint8Array, body: Uint8Array];
|
|
40
47
|
}
|
|
41
48
|
|
|
42
49
|
export function register_hooks(hooks: ModuleHooks);
|
|
@@ -73,11 +73,33 @@ const spacetime = schema({
|
|
|
73
73
|
|
|
74
74
|
const arrayRetValue = t.array(person.rowType);
|
|
75
75
|
const optionalPerson = t.option(person.rowType);
|
|
76
|
+
const multiplePrimaryKeyRows = t.array(
|
|
77
|
+
t.row('MultiplePrimaryKeyRows', {
|
|
78
|
+
id: t.u32().primaryKey(),
|
|
79
|
+
name: t.string().primaryKey(),
|
|
80
|
+
})
|
|
81
|
+
);
|
|
76
82
|
|
|
77
83
|
spacetime.anonymousView({ name: 'v1', public: true }, arrayRetValue, ctx => {
|
|
78
84
|
return ctx.from.person.build();
|
|
79
85
|
});
|
|
80
86
|
|
|
87
|
+
// @ts-expect-error views can have at most one primaryKey column on the returned row type.
|
|
88
|
+
spacetime.anonymousView(
|
|
89
|
+
{ name: 'multiplePrimaryRows', public: true },
|
|
90
|
+
multiplePrimaryKeyRows,
|
|
91
|
+
() => []
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// @ts-expect-error the same multiple-primary-key check also applies to query-builder views.
|
|
95
|
+
spacetime.anonymousView(
|
|
96
|
+
{ name: 'multiplePrimaryRowsQuery', public: true },
|
|
97
|
+
multiplePrimaryKeyRows,
|
|
98
|
+
ctx => {
|
|
99
|
+
return ctx.from.person;
|
|
100
|
+
}
|
|
101
|
+
);
|
|
102
|
+
|
|
81
103
|
spacetime.anonymousView(
|
|
82
104
|
{ name: 'optionalPerson', public: true },
|
|
83
105
|
optionalPerson,
|