@midsummer-ai/sdk 0.0.0-stage → 0.1.0-next.419

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.
Files changed (79) hide show
  1. package/LICENSE +177 -0
  2. package/README.md +4 -2
  3. package/agents/contract.d.mts +9 -0
  4. package/agents/contract.mjs +221 -0
  5. package/agents/index.d.mts +128 -0
  6. package/agents/index.mjs +203 -0
  7. package/agents/stand-in.d.mts +35 -0
  8. package/agents/stand-in.mjs +149 -0
  9. package/ai/contract.d.mts +10 -0
  10. package/ai/contract.mjs +77 -0
  11. package/ai/index.d.mts +94 -0
  12. package/ai/index.mjs +193 -0
  13. package/ai/stand-in.d.mts +7 -0
  14. package/ai/stand-in.mjs +41 -0
  15. package/build-SQ44jwSa.d.mts +41 -0
  16. package/client.d.mts +45 -0
  17. package/client.mjs +106 -0
  18. package/connection-CDk7H1FL.mjs +1871 -0
  19. package/contract.d.mts +10 -0
  20. package/contract.mjs +469 -0
  21. package/database/build.d.mts +2 -0
  22. package/database/build.mjs +579 -0
  23. package/database/client.d.mts +60 -0
  24. package/database/client.mjs +126 -0
  25. package/database/contract.d.mts +11 -0
  26. package/database/contract.mjs +164 -0
  27. package/database/errors.d.mts +23 -0
  28. package/database/errors.mjs +13 -0
  29. package/database/server.d.mts +3 -0
  30. package/database/server.mjs +320 -0
  31. package/database/stand-in.d.mts +96 -0
  32. package/database/stand-in.mjs +422 -0
  33. package/email/client.d.mts +34 -0
  34. package/email/client.mjs +131 -0
  35. package/email/contract.d.mts +11 -0
  36. package/email/contract.mjs +99 -0
  37. package/email/index.d.mts +74 -0
  38. package/email/index.mjs +59 -0
  39. package/email/smtp.d.mts +22 -0
  40. package/email/smtp.mjs +296 -0
  41. package/email/stand-in.d.mts +13 -0
  42. package/email/stand-in.mjs +51 -0
  43. package/index-DdoK0y7D.d.mts +192 -0
  44. package/index-sgdyoA2P.d.mts +212 -0
  45. package/index.d.mts +2 -0
  46. package/index.mjs +137 -0
  47. package/names-DfCBKupJ.mjs +115 -0
  48. package/package.json +152 -4
  49. package/predict-Drfm_8pO.mjs +179 -0
  50. package/queues/contract.d.mts +13 -0
  51. package/queues/contract.mjs +278 -0
  52. package/queues/index.d.mts +184 -0
  53. package/queues/index.mjs +126 -0
  54. package/queues/stand-in.d.mts +7 -0
  55. package/queues/stand-in.mjs +110 -0
  56. package/queues/wire.d.mts +9 -0
  57. package/queues/wire.mjs +199 -0
  58. package/s3.d.mts +43 -0
  59. package/s3.mjs +1003 -0
  60. package/schema-BF-Y1EVu.mjs +142 -0
  61. package/schema-DS8x1Dn8.mjs +115 -0
  62. package/schema-f7E9uzUn.d.mts +74 -0
  63. package/server-C7O7CICU.d.mts +182 -0
  64. package/socket.d.mts +67 -0
  65. package/socket.mjs +263 -0
  66. package/stand-in.d.mts +7 -0
  67. package/stand-in.mjs +185 -0
  68. package/store-BzI9at5x.mjs +237 -0
  69. package/tabs-D-P8_ci_.mjs +457 -0
  70. package/vector/contract.d.mts +10 -0
  71. package/vector/contract.mjs +635 -0
  72. package/vector/index.d.mts +2 -0
  73. package/vector/index.mjs +108 -0
  74. package/vector/stand-in.d.mts +11 -0
  75. package/vector/stand-in.mjs +169 -0
  76. package/vector/wire.d.mts +25 -0
  77. package/vector/wire.mjs +175 -0
  78. package/wire.d.mts +54 -0
  79. package/wire.mjs +731 -0
@@ -0,0 +1,126 @@
1
+ import { DatabaseError } from "./errors.mjs";
2
+ import { t as Connection } from "../connection-CDk7H1FL.mjs";
3
+ //#region src/database/client.ts
4
+ /**
5
+ * `@midsummer-ai/sdk/database`: a Project's Database, from its app or its server.
6
+ *
7
+ * import { connect } from "@midsummer-ai/sdk/database"
8
+ * import { api } from "../db/_generated/api"
9
+ *
10
+ * const db = await connect({ url, token, refresh })
11
+ * const id = await db.mutation(api.messages.send, { channel: "general", body: "hi" })
12
+ * const stop = db.watch(api.messages.list, { channel: "general" }, (messages) => render(messages))
13
+ *
14
+ * In your Deployment's server, Midsummer hands in the Databases the Environment binds as
15
+ * `context.database`: name one by its binding name, and no key is kept anywhere.
16
+ *
17
+ * const db = await connect({ database: context.database, binding: "main" })
18
+ *
19
+ * In a CI run or an Agent's run, the run's Environment binds it the same way, and the run's own
20
+ * Workload token is what proves it: no key there either. Under `midsummer dev`, every binding
21
+ * reaches the Database's Stand-in.
22
+ *
23
+ * const db = await connect({ binding: "main" })
24
+ *
25
+ * From code outside Midsummer, a Database Key is enough: it is exchanged for a token, and again
26
+ * before that token runs out.
27
+ *
28
+ * const db = await connect({ key: process.env.DATABASE_KEY! })
29
+ *
30
+ * One connection carries every call. A mutation is tagged, so one sent again after a lost
31
+ * connection applies once; a watch is given its value now and again after every change. Every
32
+ * refusal is a `DatabaseError` whose `code`, a `DatabaseErrorCode`, tells you why.
33
+ */
34
+ const API = "https://api.midsummer.new";
35
+ /** A token and the Database's address, from the platform, for a key or for a run's binding. */
36
+ async function exchange(api, proof) {
37
+ const response = await fetch(`${api}/v1/db/tokens`, {
38
+ method: "POST",
39
+ headers: { "Content-Type": "application/json" },
40
+ body: JSON.stringify(typeof proof === "string" ? { key: proof } : proof)
41
+ });
42
+ const answer = await response.json().catch(() => ({}));
43
+ if (!response.ok || !answer.token || !answer.url) throw new DatabaseError("denied", answer.error ?? `The Database Key was refused (${response.status}).`);
44
+ return {
45
+ token: answer.token,
46
+ url: answer.url
47
+ };
48
+ }
49
+ const refusal = (answer) => new DatabaseError(answer?.why ?? answer?.err ?? "refused", answer?.error ?? answer?.why ?? "refused");
50
+ const caught = (error) => ({
51
+ ok: false,
52
+ why: error?.code ?? "refused",
53
+ error: error?.message
54
+ });
55
+ async function connect(options) {
56
+ let url, token, refresh;
57
+ if ("database" in options) {
58
+ const first = await options.database.token(options.binding);
59
+ url = first.url;
60
+ token = first.token;
61
+ refresh = async () => (await options.database.token(options.binding)).token;
62
+ } else if ("binding" in options) {
63
+ const env = globalThis.process?.env ?? {};
64
+ if (env.MIDSUMMER_DATABASE_URL) {
65
+ url = env.MIDSUMMER_DATABASE_URL;
66
+ token = env.MIDSUMMER_DATABASE_TOKEN;
67
+ } else {
68
+ const workload = env.MIDSUMMER_IDENTITY_REQUEST_TOKEN, tokenUrl = env.MIDSUMMER_IDENTITY_TOKEN_URL;
69
+ if (!workload || !tokenUrl) throw new DatabaseError("denied", "This code runs outside Midsummer, so no Environment binds a Database for it. Connect with a Database Key: connect({ key }).");
70
+ const api = options.api ?? new URL(tokenUrl).origin;
71
+ const proof = {
72
+ binding: options.binding,
73
+ workload
74
+ };
75
+ const first = await exchange(api, proof);
76
+ url = first.url;
77
+ token = first.token;
78
+ refresh = async () => (await exchange(api, proof)).token;
79
+ }
80
+ } else if ("key" in options) {
81
+ const api = options.api ?? API;
82
+ const first = await exchange(api, options.key);
83
+ url = first.url;
84
+ token = first.token;
85
+ refresh = async () => (await exchange(api, options.key)).token;
86
+ } else {
87
+ url = options.url;
88
+ token = options.token;
89
+ refresh = options.refresh;
90
+ }
91
+ const connection = await Connection.connect(url, {
92
+ ...token !== void 0 ? { token } : {},
93
+ ...refresh ? { refresh } : {}
94
+ });
95
+ const call = (fn) => ({
96
+ name: fn.name,
97
+ kind: fn.kind
98
+ });
99
+ return {
100
+ async query(fn, args) {
101
+ const value = await connection.query(call(fn), args).catch(caught);
102
+ if (value && typeof value === "object" && !Array.isArray(value) && value.ok === false) throw refusal(value);
103
+ return value;
104
+ },
105
+ async mutation(fn, args, { tag } = {}) {
106
+ const answer = await connection.mutation(call(fn), args, tag ? { tag } : {}).catch(caught);
107
+ if (!answer?.ok) throw refusal(answer);
108
+ return answer.value ?? null;
109
+ },
110
+ async action(fn, args) {
111
+ const answer = await connection.mutation(call(fn), args, {}).catch(caught);
112
+ if (!answer?.ok) throw refusal(answer);
113
+ return answer.value ?? null;
114
+ },
115
+ watch(fn, args, onValue) {
116
+ return connection.watchQuery(call(fn), args, {}, (value) => {
117
+ if (value !== void 0) onValue(value);
118
+ });
119
+ },
120
+ close() {
121
+ connection.close();
122
+ }
123
+ };
124
+ }
125
+ //#endregion
126
+ export { DatabaseError, connect };
@@ -0,0 +1,11 @@
1
+ import { DatabaseClient } from "./stand-in.mjs";
2
+ //#region src/database/contract.d.ts
3
+ /** The fixture's folder: `schema.ts`, `notes.ts` and `accounts.ts`, served by every Database the cases run against. */
4
+ declare const contractFolder: URL;
5
+ interface ContractCase {
6
+ readonly name: string;
7
+ run(db: DatabaseClient): Promise<void>;
8
+ }
9
+ declare const databaseContract: ReadonlyArray<ContractCase>;
10
+ //#endregion
11
+ export { ContractCase, contractFolder, databaseContract };
@@ -0,0 +1,164 @@
1
+ import { DatabaseError } from "./errors.mjs";
2
+ //#region src/database/contract.ts
3
+ /**
4
+ * What every Database promises its client, as cases any test runner can run. The Database runs them
5
+ * through its own client and wire, and the Stand-in passes the same cases; a behaviour that is not a
6
+ * case here is not promised.
7
+ *
8
+ * Each case is given a client of a Database serving the fixture in `./contract-db` (its functions
9
+ * and schema). Expected values are written out, never computed with a Database's own code, and each
10
+ * case makes its own caller, so cases never see each other's rows.
11
+ *
12
+ * for (const item of databaseContract) test(item.name, () => item.run(client))
13
+ */
14
+ /** The fixture's folder: `schema.ts`, `notes.ts` and `accounts.ts`, served by every Database the cases run against. */
15
+ const contractFolder = new URL("./contract-db/", import.meta.url);
16
+ function eq(got, want, what) {
17
+ if (JSON.stringify(got) !== JSON.stringify(want)) throw new Error(`${what}: got ${JSON.stringify(got)}, want ${JSON.stringify(want)}`);
18
+ }
19
+ async function refusedWith(call, code, what) {
20
+ try {
21
+ await call();
22
+ } catch (error) {
23
+ if (error instanceof DatabaseError && error.code === code) return;
24
+ throw new Error(`${what}: refused ${error?.code ?? error?.message}, want ${code}`);
25
+ }
26
+ throw new Error(`${what}: answered, want refused ${code}`);
27
+ }
28
+ function caller(db) {
29
+ const subject = `c${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
30
+ return {
31
+ subject,
32
+ me: db.as(subject)
33
+ };
34
+ }
35
+ async function until(test, ms, what) {
36
+ for (const started = Date.now(); Date.now() - started < ms; await new Promise((resolve) => setTimeout(resolve, 50))) if (test()) return;
37
+ throw new Error(`${what}: not within ${ms} ms`);
38
+ }
39
+ const databaseContract = [
40
+ {
41
+ name: "a mutation's row reads back with what it wrote, owned by its caller",
42
+ async run(db) {
43
+ const { subject, me } = caller(db);
44
+ const id = await me.mutation("notes.add", { text: "hello" });
45
+ eq(await me.query("notes.get", { id }), {
46
+ owner: subject,
47
+ text: "hello",
48
+ n: 0
49
+ }, "the row");
50
+ }
51
+ },
52
+ {
53
+ name: "a mutation that throws writes nothing; one that returns writes all of it",
54
+ async run(db) {
55
+ const { me } = caller(db);
56
+ await refusedWith(() => me.mutation("notes.addTwo", {
57
+ text: "t",
58
+ fail: true
59
+ }), "refused", "the throw");
60
+ eq(await me.query("notes.mine", {}), [], "after the throw");
61
+ eq(await me.mutation("notes.addTwo", {
62
+ text: "t",
63
+ fail: false
64
+ }), 2, "its answer");
65
+ eq(await me.query("notes.mine", {}), ["t 1", "t 2"], "after the return");
66
+ }
67
+ },
68
+ {
69
+ name: "a take of more than a field holds is refused short, and nothing moves",
70
+ async run(db) {
71
+ const { me } = caller(db);
72
+ const a = await me.mutation("accounts.open", { coins: 5 }), b = await me.mutation("accounts.open", { coins: 0 });
73
+ await refusedWith(() => me.mutation("accounts.move", {
74
+ from: a,
75
+ to: b,
76
+ coins: 7
77
+ }), "short", "the move");
78
+ eq([await me.query("accounts.coins", { id: a }), await me.query("accounts.coins", { id: b })], [5, 0], "the coins");
79
+ await me.mutation("accounts.move", {
80
+ from: a,
81
+ to: b,
82
+ coins: 3
83
+ });
84
+ eq([await me.query("accounts.coins", { id: a }), await me.query("accounts.coins", { id: b })], [2, 3], "the coins after a move");
85
+ }
86
+ },
87
+ {
88
+ name: "increments made at once add up exactly",
89
+ async run(db) {
90
+ const { me } = caller(db);
91
+ const id = await me.mutation("notes.add", { text: "count" });
92
+ await Promise.all(Array.from({ length: 20 }, () => me.mutation("notes.bump", { id })));
93
+ eq((await me.query("notes.get", { id })).n, 20, "the count");
94
+ }
95
+ },
96
+ {
97
+ name: "a mutation sent twice as the same call applies once",
98
+ async run(db) {
99
+ const { me } = caller(db);
100
+ const tag = await me.tag();
101
+ await me.mutation("notes.add", { text: "once" }, { tag });
102
+ try {
103
+ await me.mutation("notes.add", { text: "once" }, { tag });
104
+ } catch (error) {
105
+ if (!(error instanceof DatabaseError)) throw error;
106
+ }
107
+ eq(await me.query("notes.mine", {}), ["once"], "the rows");
108
+ }
109
+ },
110
+ {
111
+ name: "arguments that do not fit the function are refused invalid",
112
+ async run(db) {
113
+ const { me } = caller(db);
114
+ await refusedWith(() => me.mutation("notes.add", { text: 5 }), "invalid", "a number for text");
115
+ await refusedWith(() => me.mutation("notes.addTwo", { text: "x" }), "invalid", "a missing argument");
116
+ }
117
+ },
118
+ {
119
+ name: "a write that does not fit the schema is refused invalid, and nothing is written",
120
+ async run(db) {
121
+ const { me } = caller(db);
122
+ await refusedWith(() => me.mutation("notes.addRaw", { text: 42 }), "invalid", "a number in a text field");
123
+ eq(await me.query("notes.mine", {}), [], "the rows");
124
+ }
125
+ },
126
+ {
127
+ name: "an internal function is refused to a client",
128
+ async run(db) {
129
+ await refusedWith(() => caller(db).me.query("notes.secret", {}), "internal", "the internal query");
130
+ }
131
+ },
132
+ {
133
+ name: "a function that is not there is refused",
134
+ async run(db) {
135
+ await refusedWith(() => caller(db).me.query("notes.nothing", {}), "no function", "the call");
136
+ }
137
+ },
138
+ {
139
+ name: "a function's caller is the signed-in subject",
140
+ async run(db) {
141
+ const { subject, me } = caller(db);
142
+ eq(await me.query("notes.whoami", {}), subject, "the subject");
143
+ }
144
+ },
145
+ {
146
+ name: "a watched query is given its value again after a mutation changes it",
147
+ async run(db) {
148
+ const { me } = caller(db);
149
+ let seen = null;
150
+ const stop = me.watch("notes.mine", {}, (value) => {
151
+ seen = value;
152
+ });
153
+ try {
154
+ await until(() => Array.isArray(seen), 5e3, "its first value");
155
+ await me.mutation("notes.add", { text: "pushed" });
156
+ await until(() => JSON.stringify(seen) === JSON.stringify(["pushed"]), 5e3, "the new value");
157
+ } finally {
158
+ stop();
159
+ }
160
+ }
161
+ }
162
+ ];
163
+ //#endregion
164
+ export { contractFolder, databaseContract };
@@ -0,0 +1,23 @@
1
+ //#region src/database/errors.d.ts
2
+ /**
3
+ * Why a Database refused a call, as the client throws it: the same codes from the Stand-in and the
4
+ * Database.
5
+ *
6
+ * - `refused`: the function threw; nothing it wrote was kept.
7
+ * - `short`: a `take` asked for more than a field holds; nothing moved.
8
+ * - `notfound`: a patch or replace named a row that is not there.
9
+ * - `invalid`: the arguments, the answer or a write did not fit their shapes.
10
+ * - `internal`: an internal function, which no client may call.
11
+ * - `no function`: no function of that name.
12
+ * - `busy`, `quota`, `paused`: a limit the Project's plan or budget sets; `busy` may be sent again.
13
+ * - `denied`: the caller may not do this: a Database Key or token was refused, or a token that only reads asked to write.
14
+ * - `unavailable`: the Database could not be reached, or could not finish the call.
15
+ */
16
+ type DatabaseErrorCode = "refused" | "short" | "notfound" | "invalid" | "internal" | "no function" | "busy" | "quota" | "paused" | "denied" | "unavailable";
17
+ declare class DatabaseError extends Error {
18
+ readonly code: DatabaseErrorCode | (string & {});
19
+ readonly detail?: unknown;
20
+ constructor(code: DatabaseErrorCode | (string & {}), message?: string, detail?: unknown);
21
+ }
22
+ //#endregion
23
+ export { DatabaseError, DatabaseErrorCode };
@@ -0,0 +1,13 @@
1
+ //#region src/database/errors.ts
2
+ var DatabaseError = class extends Error {
3
+ code;
4
+ detail;
5
+ constructor(code, message = code, detail) {
6
+ super(message);
7
+ this.code = code;
8
+ this.detail = detail;
9
+ this.name = "DatabaseError";
10
+ }
11
+ };
12
+ //#endregion
13
+ export { DatabaseError };
@@ -0,0 +1,3 @@
1
+ import { a as defineSchema, c as shapeError, i as Table, l as v, n as Schema, o as defineTable, r as Shape, s as indexFields, t as IndexDeclaration } from "../schema-f7E9uzUn.mjs";
2
+ import { C as take, S as query, _ as internalAction, a as InsufficientError, b as isFunction, c as RangeOptions, d as Row, f as Rows, g as increment, h as give, i as FunctionReference, l as Refusal, m as action, n as Document, o as NotFoundError, p as ScheduledRun, r as FunctionKind, s as Operator, t as Capabilities, u as RegisteredFunction, v as internalMutation, x as mutation, y as internalQuery } from "../server-C7O7CICU.mjs";
3
+ export { Capabilities, Document, FunctionKind, FunctionReference, type IndexDeclaration, InsufficientError, NotFoundError, Operator, RangeOptions, Refusal, RegisteredFunction, Row, Rows, ScheduledRun, type Schema, type Shape, type Table, action, defineSchema, defineTable, give, increment, indexFields, internalAction, internalMutation, internalQuery, isFunction, mutation, query, shapeError, take, v };
@@ -0,0 +1,320 @@
1
+ import { a as shapeError, i as indexFields, n as defineSchema, o as v, r as defineTable, t as TABLE } from "../schema-BF-Y1EVu.mjs";
2
+ //#region src/database/server.ts
3
+ /**
4
+ * `@midsummer-ai/sdk/server`: what a Project's `db/` functions are written with. Each file of `db/` is
5
+ * a module of functions, each export a function named `<file>.<export>`, referred to by the client
6
+ * as `api.<file>.<export>`:
7
+ *
8
+ * import { mutation, query, v } from "./_generated/server"
9
+ *
10
+ * export const send = mutation({
11
+ * args: { channel: v.string(), body: v.string() },
12
+ * handler: async (ctx, { channel, body }) => ctx.db.messages.insert({ channel, body }),
13
+ * })
14
+ * export const list = query({
15
+ * args: { channel: v.string() },
16
+ * handler: async (ctx, { channel }) => ctx.db.messages.range("by_channel", "channel", { eq: channel }).rows,
17
+ * })
18
+ *
19
+ * `./_generated/server` is this module typed by the Project's schema; `db/schema.ts` is
20
+ * `export default defineSchema({ ... })`.
21
+ *
22
+ * You write what you mean, and the Database does the concurrency. A query reads rows and never writes them;
23
+ * watched, it is run again when anything it read changes. A mutation is one serializable step: its
24
+ * reads see the rows as they are, its writes are saved together or not at all, and it is run again when
25
+ * a row it read changed, so there is no conflict to handle and no retry loop. A mutation sent again
26
+ * under the same call tag applies once. Neither sees a clock, the network or randomness: `ctx.now`
27
+ * is the call's own time, and its body may run more than once. An action may wait on the network
28
+ * (`ctx.fetch`, https only); its writes are the mutations it runs with `ctx.run`.
29
+ *
30
+ * ctx.db.<table>: get(id), range(index, field, { eq, gte, gt, lte, lt, order, limit }), where
31
+ * index "" is the table's own order by id; in a mutation also insert(doc) (its id at once),
32
+ * patch(id, fields) (a field's value may be `increment(n)`, `take(n)` or `give(n)`),
33
+ * replace(id, doc) and delete(id). A document is `{ _id, _version, ...fields }`.
34
+ * ctx.auth: the signed-in caller, `{ subject, issuer }`, or null.
35
+ * ctx.scheduler: runAfter(ms, fn, args), runAt(time, fn, args), status(id), cancel(id).
36
+ * cron: "minute hour day month weekday" (UTC) on a mutation or an action runs it on a schedule.
37
+ */
38
+ const FUNCTION = /^[A-Za-z][A-Za-z0-9_]{0,63}(?:\.[A-Za-z][A-Za-z0-9_]{0,63})?$/u;
39
+ function defineFunction(name, handler, options) {
40
+ if (name !== null && (typeof name !== "string" || !FUNCTION.test(name))) throw new TypeError("a function's name: [A-Za-z][A-Za-z0-9_]{0,63}");
41
+ const { kind, args = null, returns = null, internal = false, cron = null } = options;
42
+ if (args !== null && (typeof args !== "object" || Array.isArray(args))) throw new TypeError(`${name}: args is an object of shapes (v.string(), ...)`);
43
+ if (cron !== null && (kind === "query" || typeof cron !== "string" || cron.trim().split(/\s+/u).length !== 5 || !/^[0-9*/,\- ]{9,100}$/u.test(cron.trim()))) throw new TypeError(`${name}: cron is "minute hour day month weekday" (a mutation or an action)`);
44
+ return Object.freeze({
45
+ name,
46
+ handler,
47
+ options: Object.freeze({
48
+ kind,
49
+ args: args && Object.freeze({ object: args }),
50
+ returns,
51
+ internal: internal === true,
52
+ cron: cron && cron.trim()
53
+ })
54
+ });
55
+ }
56
+ const OPERATOR = Symbol("operator");
57
+ function operator(kind, n) {
58
+ if (!Number.isFinite(n) || n < 0) throw new TypeError(`${kind}(n): n is a number of at least 0`);
59
+ return Object.freeze({
60
+ [OPERATOR]: kind,
61
+ n
62
+ });
63
+ }
64
+ /** A field's value plus `n` (a field not yet there counts as 0). */
65
+ const increment = (n = 1) => operator("increment", n);
66
+ /** A field's value minus `n`; below zero the whole mutation fails with `InsufficientError` and writes nothing. */
67
+ const take = (n = 1) => operator("take", n);
68
+ /** A field's value plus `n`: the inverse of `take`. */
69
+ const give = (n = 1) => operator("give", n);
70
+ /** A `take` asked for more than the field holds: the mutation wrote nothing. */
71
+ var InsufficientError = class extends Error {
72
+ detail;
73
+ constructor(detail) {
74
+ super(`not enough ${detail.table}.${detail.field}: has ${detail.has}, asked ${detail.asked}`);
75
+ this.detail = detail;
76
+ this.name = "InsufficientError";
77
+ }
78
+ };
79
+ /** A mutation patched or replaced a row that is not there. */
80
+ var NotFoundError = class extends Error {
81
+ detail;
82
+ constructor(detail) {
83
+ super(`no ${detail.table} row ${detail.id}`);
84
+ this.detail = detail;
85
+ this.name = "NotFoundError";
86
+ }
87
+ };
88
+ /** A call the Database refused: `why` is its reason, as the client's `DatabaseError` carries it. */
89
+ var Refusal = class extends Error {
90
+ why;
91
+ detail;
92
+ constructor(why, message, detail) {
93
+ super(message);
94
+ this.why = why;
95
+ this.detail = detail;
96
+ this.name = "Refusal";
97
+ }
98
+ };
99
+ const documentOf = (id, row) => {
100
+ if (row === null || row === void 0) return null;
101
+ const fields = Object.fromEntries(Object.entries(row).filter(([key]) => !key.startsWith("_")));
102
+ return Object.freeze({
103
+ ...fields,
104
+ _id: id,
105
+ _version: Number(row._version ?? 0)
106
+ });
107
+ };
108
+ const fieldsOf = (doc) => Object.fromEntries(Object.entries(doc ?? {}).filter(([key]) => !key.startsWith("_")));
109
+ function applied(current, value, where) {
110
+ if (value === null || typeof value !== "object" || !(OPERATOR in value)) return value;
111
+ const op = value;
112
+ const has = current === void 0 || current === null ? 0 : Number(current);
113
+ if (!Number.isFinite(has)) throw new TypeError(`${where.table}.${where.field} is not a number: ${op[OPERATOR]}() needs one`);
114
+ if (op[OPERATOR] === "take") {
115
+ if (has - op.n < 0) throw new InsufficientError({
116
+ ...where,
117
+ has,
118
+ asked: op.n
119
+ });
120
+ return has - op.n;
121
+ }
122
+ return has + op.n;
123
+ }
124
+ function tablesOf(make) {
125
+ const cache = /* @__PURE__ */ new Map();
126
+ const table = (name) => {
127
+ if (typeof name !== "string" || !TABLE.test(name)) throw new TypeError(`a table name: ${JSON.stringify(name)}`);
128
+ if (!cache.has(name)) cache.set(name, make(name));
129
+ return cache.get(name);
130
+ };
131
+ return new Proxy(Object.freeze({ table }), { get: (target, key) => key in target || typeof key !== "string" ? target[key] : table(key) });
132
+ }
133
+ const boundsOf = (options = {}) => ({
134
+ ...options.eq === void 0 ? {} : { eq: [options.eq] },
135
+ gte: options.gte,
136
+ gt: options.gt,
137
+ lte: options.lte,
138
+ lt: options.lt,
139
+ ...options.order ? { order: options.order } : {},
140
+ limit: options.limit ?? 1e3
141
+ });
142
+ const rangeOf = (rows, name, index, _field, options) => ({
143
+ ok: true,
144
+ rows: rows.range(name, index || "_id", boundsOf(options)).map((row) => documentOf(row._id, row))
145
+ });
146
+ const readDb = (rows) => tablesOf((name) => Object.freeze({
147
+ get: (id) => documentOf(String(id), rows.get(name, String(id))),
148
+ range: (index, field, options) => rangeOf(rows, name, index, field, options)
149
+ }));
150
+ /** A short, stable text of a string (FNV-1a, base 36): the ids a call inserts. */
151
+ function fnv(text) {
152
+ let hash = 2166136261;
153
+ for (let i = 0; i < text.length; i += 1) hash = Math.imul(hash ^ text.charCodeAt(i), 16777619) >>> 0;
154
+ return hash.toString(36);
155
+ }
156
+ /** A write's document checked against its table in the schema; a table the schema does not declare takes no writes. */
157
+ function fits(schema, name, doc) {
158
+ if (!schema) return;
159
+ const fields = schema[name];
160
+ if (!fields) throw new Refusal("invalid", `${name}: no such table in the schema`, { table: name });
161
+ const why = shapeError({ object: fields }, doc, name);
162
+ if (why) throw new Refusal("invalid", why, { table: name });
163
+ }
164
+ function writeDb(rows) {
165
+ let inserts = 0;
166
+ return tablesOf((name) => Object.freeze({
167
+ get: (id) => documentOf(String(id), rows.get(name, String(id))),
168
+ range: (index, field, options) => rangeOf(rows, name, index, field, options),
169
+ insert: (doc) => {
170
+ const id = `${fnv(`${rows.call}:${name}`)}${(inserts++).toString(36).padStart(2, "0")}`;
171
+ const fields = fieldsOf(doc);
172
+ fits(rows.schema, name, fields);
173
+ rows.set(name, id, fields);
174
+ return id;
175
+ },
176
+ patch: (id, change) => {
177
+ const row = rows.get(name, String(id));
178
+ if (row === null || row === void 0) throw new NotFoundError({
179
+ table: name,
180
+ id
181
+ });
182
+ const current = fieldsOf(row);
183
+ const next = { ...current };
184
+ for (const [field, value] of Object.entries(change ?? {})) if (value === void 0) delete next[field];
185
+ else next[field] = applied(current[field], value, {
186
+ table: name,
187
+ id,
188
+ field
189
+ });
190
+ fits(rows.schema, name, next);
191
+ rows.set(name, String(id), next);
192
+ },
193
+ replace: (id, doc) => {
194
+ if (rows.get(name, String(id)) === null) throw new NotFoundError({
195
+ table: name,
196
+ id
197
+ });
198
+ const fields = fieldsOf(doc);
199
+ fits(rows.schema, name, fields);
200
+ rows.set(name, String(id), fields);
201
+ },
202
+ delete: (id) => {
203
+ rows.delete(name, String(id));
204
+ }
205
+ }));
206
+ }
207
+ function spec(kind, name, given) {
208
+ if (given === null || typeof given !== "object" || typeof given.handler !== "function") throw new TypeError(`${kind}(${name ? `"${name}", ` : ""}{ args, returns, cron, handler })`);
209
+ return given;
210
+ }
211
+ /** A definer's arguments: `({ ... })`, named by its export, or `("name", { ... })`. */
212
+ const named = (name, given) => given === void 0 && name !== null && typeof name === "object" ? [null, name] : [name, given];
213
+ const fnName = (fn) => fn !== null && typeof fn === "object" && typeof fn.name === "string" ? fn.name : String(fn);
214
+ function schedulerOf(rows) {
215
+ return Object.freeze({
216
+ runAfter: (ms, fn, args) => rows.runAfter(Math.max(0, Number(ms) || 0), fnName(fn), args ?? null),
217
+ runAt: (at, fn, args) => rows.runAfter(Math.max(0, (Number(at) || 0) - rows.now), fnName(fn), args ?? null),
218
+ status: (id) => {
219
+ const row = rows.scheduledRow(String(id));
220
+ return row ? Object.freeze({
221
+ name: row.name,
222
+ args: row.args ?? null,
223
+ at: row.due,
224
+ state: row.state,
225
+ ...row.error ? { error: row.error } : {}
226
+ }) : null;
227
+ },
228
+ cancel: (id) => rows.cancelScheduled(String(id))
229
+ });
230
+ }
231
+ const authOf = (user, issuer) => user ? Object.freeze({
232
+ subject: user,
233
+ issuer: issuer ?? null
234
+ }) : null;
235
+ const contextOf = (rows, tables, scheduler) => Object.freeze({
236
+ db: tables,
237
+ identity: Object.freeze({
238
+ user: rows.user ?? null,
239
+ server: !!rows.server
240
+ }),
241
+ auth: authOf(rows.server ? null : rows.user, rows.issuer),
242
+ now: rows.now,
243
+ ...scheduler ? { scheduler } : {}
244
+ });
245
+ /** A function that reads rows and never writes them: `export const list = query({ args, returns, handler: (ctx, args) => ... })`. */
246
+ function query(name, given, extra = {}) {
247
+ const [own, definition] = named(name, given);
248
+ const { handler, args = null, returns = null } = spec("query", own, definition);
249
+ return defineFunction(own, (rows, a) => handler(contextOf(rows, readDb(rows)), a ?? {}), {
250
+ kind: "query",
251
+ args,
252
+ returns,
253
+ ...extra
254
+ });
255
+ }
256
+ /** One serializable transaction: `export const send = mutation({ args, returns, handler: (ctx, args) => ... })`. */
257
+ function mutation(name, given, extra = {}) {
258
+ const [own, definition] = named(name, given);
259
+ const { handler, args = null, returns = null, cron = null } = spec("mutation", own, definition);
260
+ return defineFunction(own, (rows, a) => handler(contextOf(rows, writeDb(rows), schedulerOf(rows)), a ?? {}), {
261
+ kind: "mutation",
262
+ args,
263
+ returns,
264
+ cron,
265
+ ...extra
266
+ });
267
+ }
268
+ /** May wait on the network: `export const charge = action({ args, returns, handler: async (ctx, args) => ... })`. */
269
+ function action(name, given, extra = {}) {
270
+ const [own, definition] = named(name, given);
271
+ const { handler, args = null, returns = null, cron = null } = spec("action", own, definition);
272
+ return defineFunction(own, (cap, a) => {
273
+ const tables = tablesOf((table) => Object.freeze({
274
+ get: async (id) => documentOf(String(id), await cap.get(table, String(id))),
275
+ range: async (index, _field, options) => ({
276
+ ok: true,
277
+ rows: (await cap.range(table, index || "_id", boundsOf(options))).map((row) => documentOf(row._id, row))
278
+ })
279
+ }));
280
+ const ctx = Object.freeze({
281
+ db: tables,
282
+ identity: Object.freeze({
283
+ user: cap.user ?? null,
284
+ server: !!cap.server
285
+ }),
286
+ auth: authOf(cap.server ? null : cap.user, cap.issuer),
287
+ fetch: async (url, init) => {
288
+ const target = new URL(String(url));
289
+ if (target.protocol !== "https:") throw new TypeError("ctx.fetch: an https URL");
290
+ return cap.fetch(target, init);
291
+ },
292
+ now: () => cap.now(),
293
+ random: (n = 16) => cap.random(n),
294
+ run: (fn, args) => cap.run(fnName(fn), args ?? null),
295
+ scheduler: Object.freeze({
296
+ runAfter: (ms, fn, args) => cap.schedule(Math.max(0, Number(ms) || 0), fnName(fn), args ?? null),
297
+ runAt: (at, fn, args) => cap.schedule(Math.max(0, (Number(at) || 0) - cap.now()), fnName(fn), args ?? null),
298
+ status: (id) => cap.scheduled(String(id)),
299
+ cancel: (id) => cap.cancel(String(id))
300
+ })
301
+ });
302
+ return handler(ctx, a ?? {});
303
+ }, {
304
+ kind: "action",
305
+ args,
306
+ returns,
307
+ cron,
308
+ ...extra
309
+ });
310
+ }
311
+ /** As `query`, callable only by another function, never a client. */
312
+ const internalQuery = (name, given) => query(name, given, { internal: true });
313
+ /** As `mutation`, callable only by another function, never a client. */
314
+ const internalMutation = (name, given) => mutation(name, given, { internal: true });
315
+ /** As `action`, callable only by another function, never a client. */
316
+ const internalAction = (name, given) => action(name, given, { internal: true });
317
+ /** Whether a value is a function defined here, as a module of `db/` exports it. */
318
+ const isFunction = (value) => value !== null && typeof value === "object" && typeof value.handler === "function" && typeof value.options?.kind === "string";
319
+ //#endregion
320
+ export { InsufficientError, NotFoundError, Refusal, action, defineSchema, defineTable, give, increment, indexFields, internalAction, internalMutation, internalQuery, isFunction, mutation, query, shapeError, take, v };