@naru-pub/dev 0.0.0-stage → 1.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.
@@ -0,0 +1,206 @@
1
+ /**
2
+ * What Naru hands a site's own code (SDK 1.1.0): queries in
3
+ * `_naru/queries/`, mutations in `_naru/mutations/` and routes in
4
+ * `_naru/routes.js`, run on Naru over the tables `_naru/migrations/*.sql`
5
+ * create. For type checking only; nothing here exists at run time.
6
+ *
7
+ * ```ts
8
+ * import type { Query, Mutation } from "./functions.d.ts";
9
+ * export const posts: Query<{ board: string }> = async (ctx, { board }) =>
10
+ * (await ctx.sql`SELECT id, title FROM posts WHERE board = ${board}`).rows;
11
+ * ```
12
+ *
13
+ * The code runs in QuickJS, not a browser or Node. Its globals are the
14
+ * language's own (Object, Array, JSON, Math, Date, RegExp, Map, Set, WeakMap,
15
+ * WeakSet, WeakRef, Promise, Proxy, Reflect, Symbol, BigInt, Iterator,
16
+ * ArrayBuffer, DataView, the typed arrays, the Error types, the URI
17
+ * functions, parseInt and the like) and: URL and URLSearchParams, as
18
+ * browsers have them; structuredClone, for primitives, plain objects and
19
+ * arrays, Date, RegExp, Map, Set, ArrayBuffer and Uint8Array (anything else
20
+ * throws a DataCloneError); Math.random; Date.now, the server's clock (prefer
21
+ * `ctx.now`; local time is the server's, so use UTC methods);
22
+ * performance.now, queueMicrotask and DOMException. There is no Intl
23
+ * (toLocaleString formats as English), TextEncoder, TextDecoder, atob, btoa,
24
+ * crypto, console, fetch, setTimeout or other timers, WebAssembly, require or
25
+ * process, whatever your editor's lib types say.
26
+ *
27
+ * Written by hand beside the generated naru.d.ts, whose `Json` this repeats
28
+ * so this file stands on its own.
29
+ * @packageDocumentation
30
+ */
31
+
32
+ export type Json =
33
+ null | boolean | number | string | Json[] | { [key: string]: Json };
34
+
35
+ /** The failure codes `ctx.fail` takes, as pages receive them in `error.code`. */
36
+ export type ErrorCode =
37
+ | "CONFLICT"
38
+ | "QUOTA_EXCEEDED"
39
+ | "AUTH_REQUIRED"
40
+ | "ACCESS_DENIED"
41
+ | "NOT_FOUND"
42
+ | "RATE_LIMITED"
43
+ | "INVALID_REQUEST"
44
+ | "UNAVAILABLE";
45
+
46
+ /**
47
+ * The member who called: their ID on this site, the same on every visit.
48
+ * What they may do is the site's to keep, in its own tables.
49
+ */
50
+ export interface CallingMember {
51
+ readonly id: string;
52
+ }
53
+
54
+ /** A value site SQL binds or returns (a boolean binds as 1 or 0). */
55
+ export type SqlValue = string | number | boolean | Uint8Array | null;
56
+ /** What `ctx.sql` answers: the statement's rows, and how many it changed. */
57
+ export interface SqlResult<Row> {
58
+ rows: Row[];
59
+ changes: number;
60
+ }
61
+
62
+ /**
63
+ * A query's or route's `ctx`: who calls, and the site's own tables through
64
+ * `sql`. Anyone may call one, signed in or not, so its code decides what
65
+ * each caller may see. A query's `sql` only reads. A call runs in one
66
+ * transaction, for tens of milliseconds in 32 MiB, makes at most 100
67
+ * database operations, 1,000 URL parses and 2,000 Math.random() draws,
68
+ * reads at most 500,000 rows in all, and returns at most 1 MiB of JSON. Every method's promise must be
69
+ * used (awaited, returned or chained), or the call fails.
70
+ */
71
+ export interface QueryCtx {
72
+ /** The signed-in member, or null for the owner and visitors. */
73
+ readonly member: CallingMember | null;
74
+ /** The site's owner called. A visitor, signed in to nothing, has neither. */
75
+ readonly owner: boolean;
76
+ /** The site's owner's Naru login, whoever calls: the site's own name. */
77
+ readonly ownerName: string;
78
+ /** The call's time in milliseconds, fixed for the whole call. */
79
+ readonly now: number;
80
+ /**
81
+ * Refuses the request: nothing the call wrote is kept, and the page's
82
+ * `error.message` is `message` (1–1000 characters) as written, so write it
83
+ * for visitors. `details`, at most 4 KiB of JSON, arrives as
84
+ * `error.details`.
85
+ */
86
+ fail(code: ErrorCode, message: string, details?: Json): never;
87
+ /**
88
+ * One statement of SQL against the site's own tables, which its
89
+ * `_naru/migrations/*.sql` create. As a tag, ``ctx.sql`... ${value}` ``
90
+ * binds every interpolated value as a parameter; given text and values,
91
+ * it runs a query builder's compiled statement. SELECT, INSERT, UPDATE and
92
+ * DELETE run; a query only reads. Naru's own tables, pragmas, triggers,
93
+ * transactions and recursion are refused, but Naru's view
94
+ * `_naru_uploads (id, url, member, name, type, size, created_at)`, the
95
+ * site's finished uploads (`member` null for the owner's), may be read
96
+ * and joined. A statement is stopped once
97
+ * SQLite works on one step of it for 3 seconds; one stopped 3 times within
98
+ * a minute is paused for 5 minutes. It binds at most 100 values, returns at most 1,000 rows and 1 MiB, BLOB columns as
99
+ * Uint8Arrays, and counts as one database operation; a call's statements
100
+ * read at most 500,000 rows. A value a UNIQUE column already holds fails
101
+ * the call with CONFLICT, `details.unique` naming the column; any other
102
+ * mistake SQLite refuses, with its message.
103
+ *
104
+ * To stay fast, read a table whole at most once and reach every other
105
+ * table by key (join on an indexed column):
106
+ *
107
+ * - A list as one value: `WHERE id IN (SELECT value FROM json_each(?))`,
108
+ * bound as `JSON.stringify(ids)`.
109
+ * - A row's JSON column: `FROM visits, json_each(visits.hours) j`.
110
+ * - An ordered aggregate per row: `(SELECT json_group_array(title) FROM
111
+ * (SELECT title FROM titles WHERE ch = c.id ORDER BY ord))`, with an
112
+ * index on `titles (ch, ord)`.
113
+ * - Past 1,000 rows: `WHERE id > ? ORDER BY id LIMIT 1000`, then again
114
+ * after the last `id`.
115
+ */
116
+ sql<Row = Record<string, SqlValue>>(
117
+ strings: TemplateStringsArray,
118
+ ...values: SqlValue[]
119
+ ): Promise<SqlResult<Row>>;
120
+ sql<Row = Record<string, SqlValue>>(
121
+ text: string,
122
+ values?: SqlValue[],
123
+ ): Promise<SqlResult<Row>>;
124
+ /**
125
+ * Guests' passwords, kept by the site as hashes. `verify` allows ten wrong
126
+ * passwords an hour for each `key` (such as "post:42"), whoever tries,
127
+ * then fails with RATE_LIMITED. A call makes at most 3 password checks.
128
+ */
129
+ passwords: {
130
+ verify(password: string, hash: string, key: string): Promise<boolean>;
131
+ };
132
+ }
133
+
134
+ /** A mutation's `ctx`: a query's, writing, and notifying members. */
135
+ export interface MutationCtx extends QueryCtx {
136
+ passwords: QueryCtx["passwords"] & {
137
+ /** A hash of `password` (1–256 characters) to keep in a table. */
138
+ hash(password: string): Promise<string>;
139
+ };
140
+ /**
141
+ * Notifies these members (at most 1,000 a call) in the Naru app, once the
142
+ * mutation returns: on their devices if they allowed the site there, or in
143
+ * the app's inbox until they do. Unknown, pending and banned members are
144
+ * skipped, and the site is not told who has the app. `path` (default "/")
145
+ * is a page of this site, opened when the notification is tapped. A title
146
+ * longer than 100 characters or a body longer than 500 is cut to fit.
147
+ * Notifications are best effort: one Naru cannot queue is dropped, and the
148
+ * mutation goes on. Member ids that are not ids, a blank title or another
149
+ * site's address still throw.
150
+ */
151
+ notify(
152
+ members: string[],
153
+ message: { title: string; body?: string; path?: string },
154
+ ): Promise<null>;
155
+ }
156
+
157
+ /** `export async function name(ctx, args)` in `_naru/queries/` or `_naru/mutations/`. */
158
+ export type Query<Args = Json, Result = Json> = (
159
+ ctx: QueryCtx,
160
+ args: Args,
161
+ ) => Result | Promise<Result>;
162
+ export type Mutation<Args = Json, Result = Json> = (
163
+ ctx: MutationCtx,
164
+ args: Args,
165
+ ) => Result | Promise<Result>;
166
+
167
+ /** What a route is asked. */
168
+ export interface RouteRequest {
169
+ /** The path as asked, still percent-encoded. */
170
+ path: string;
171
+ /** The subdomain or custom domain it was asked on, such as `https://alice.naru.pub`. */
172
+ origin: string;
173
+ /** The query string's values, the last of a repeated name. */
174
+ query: Record<string, string>;
175
+ }
176
+ /**
177
+ * What a route answers: a string of HTML, a body of one of these types, one
178
+ * of the site's files with `head` added to its `<head>` when it is HTML, or a
179
+ * redirect to a path or an `https://` URL. `status` is 200–599 (a redirect's
180
+ * 301, 302, 303, 307 or 308, by default 302); `cache` the seconds (0–300) the
181
+ * edge may keep the page, by default 0.
182
+ */
183
+ export type RouteAnswer =
184
+ | string
185
+ | ({ status?: number; cache?: number } & (
186
+ | {
187
+ body: string;
188
+ type:
189
+ | "text/html"
190
+ | "text/plain"
191
+ | "text/css"
192
+ | "text/calendar"
193
+ | "text/xml"
194
+ | "application/xml"
195
+ | "application/rss+xml"
196
+ | "application/atom+xml"
197
+ | "application/json";
198
+ }
199
+ | { file: string; head?: string }
200
+ | { redirect: string }
201
+ ));
202
+ /** `export default async function (ctx, request)` in `_naru/routes.js`. */
203
+ export type Route = (
204
+ ctx: QueryCtx,
205
+ request: RouteRequest,
206
+ ) => RouteAnswer | Promise<RouteAnswer>;
@@ -0,0 +1,349 @@
1
+ /**
2
+ * Naru Data SDK 1.1.0: browser SDK for data owned by one Naru site: 1.0.0's
3
+ * collections, with the site's own tables reached through its queries and
4
+ * mutations, live queries, and its members.
5
+ * @packageDocumentation
6
+ */
7
+
8
+ export type Json =
9
+ null | boolean | number | string | Json[] | { [key: string]: Json };
10
+
11
+ /** @internal */
12
+ declare const revisionBrand: unique symbol;
13
+ /** An opaque concurrency token. Store and return it unchanged. */
14
+ export type Revision = string & { readonly [revisionBrand]: true };
15
+
16
+ /** A read or successful write: user data stays separate from server metadata. */
17
+ export interface Document<T = Json> {
18
+ id: string;
19
+ data: T;
20
+ createdAt: string;
21
+ updatedAt: string;
22
+ revision: Revision;
23
+ }
24
+
25
+ export interface Page<T = Json> {
26
+ documents: Document<T>[];
27
+ nextCursor: string | null;
28
+ totalCount?: number;
29
+ }
30
+
31
+ /**
32
+ * Why an operation failed. The list is closed for v1: a code a later server
33
+ * adds reaches this version as the nearest code here.
34
+ */
35
+ export type NaruErrorCode =
36
+ | "CONFLICT"
37
+ | "QUOTA_EXCEEDED"
38
+ | "AUTH_REQUIRED"
39
+ | "ACCESS_DENIED"
40
+ | "NOT_FOUND"
41
+ | "RATE_LIMITED"
42
+ | "INVALID_REQUEST"
43
+ | "UNAVAILABLE";
44
+
45
+ /**
46
+ * A Naru operation that could not be completed. Check it with `instanceof`.
47
+ *
48
+ * `message` is a short Korean sentence a visitor can read, so a page may show
49
+ * it as is (`status.textContent = error.message`): a query's or mutation's
50
+ * own `ctx.fail` message, or Naru's wording for `code`. `cause` is a network
51
+ * failure's original error. What Naru answered is in the browser's network tab.
52
+ */
53
+ export declare class NaruError extends Error {
54
+ private constructor();
55
+ readonly code: NaruErrorCode;
56
+ /**
57
+ * The JSON a query or mutation passed as `ctx.fail(code, message, details)`,
58
+ * such as `{ reason: "sold_out" }`; or, when a member's request is refused
59
+ * because the site has not approved them, `{ membership: "pending" }` or
60
+ * `{ membership: "banned" }`; or, for a `CONFLICT` because another
61
+ * document already holds a value of one of the collection's unique
62
+ * fields, `{ unique: "<field>" }`. Undefined for every other failure.
63
+ */
64
+ readonly details?: Json;
65
+ }
66
+
67
+ /** Top-level user fields, combined with AND. */
68
+ export type Filter = Record<
69
+ string,
70
+ | string
71
+ | number
72
+ | boolean
73
+ | null
74
+ | {
75
+ gt?: string | number;
76
+ gte?: string | number;
77
+ lt?: string | number;
78
+ lte?: string | number;
79
+ }
80
+ >;
81
+
82
+ /** One or two keys: a user field by name, or document metadata. */
83
+ export type Sort =
84
+ | readonly [
85
+ readonly [
86
+ string | { metadata: "id" | "createdAt" | "updatedAt" },
87
+ "asc" | "desc",
88
+ ],
89
+ ]
90
+ | readonly [
91
+ readonly [
92
+ string | { metadata: "id" | "createdAt" | "updatedAt" },
93
+ "asc" | "desc",
94
+ ],
95
+ readonly [
96
+ string | { metadata: "id" | "createdAt" | "updatedAt" },
97
+ "asc" | "desc",
98
+ ],
99
+ ];
100
+
101
+ export interface ListOptions {
102
+ filter?: Filter;
103
+ sort?: Sort;
104
+ /** Default 50; maximum 100. */
105
+ size?: number;
106
+ /** Opaque cursor returned by the preceding page. */
107
+ after?: string | null;
108
+ includeTotal?: boolean;
109
+ signal?: AbortSignal;
110
+ }
111
+
112
+ /** Write only at this revision, or only if the document does not exist yet. */
113
+ export type WriteCondition = { revision: Revision } | { absent: true };
114
+
115
+ export interface PublicCollection<T = Json> {
116
+ get(id: string, options?: { signal?: AbortSignal }): Promise<Document<T>>;
117
+ list(options?: ListOptions): Promise<Page<T>>;
118
+ /** How many documents match `filter`, or the whole collection. */
119
+ count(options?: { filter?: Filter; signal?: AbortSignal }): Promise<number>;
120
+ /**
121
+ * Every page of a query in turn, following `nextCursor` from `after` (or
122
+ * the start). Not a snapshot: writes in between can shift later pages.
123
+ */
124
+ pages(options?: ListOptions): AsyncIterable<Page<T>>;
125
+ add(data: T, options?: { signal?: AbortSignal }): Promise<Document<T>>;
126
+ }
127
+
128
+ export interface AdminCollection<T = Json> extends PublicCollection<T> {
129
+ /** Replaces the whole document or creates it. */
130
+ set(
131
+ id: string,
132
+ data: T,
133
+ options?: { condition?: WriteCondition; signal?: AbortSignal },
134
+ ): Promise<Document<T>>;
135
+ /** Deleting a missing document succeeds unless a condition was supplied. */
136
+ delete(
137
+ id: string,
138
+ options?: { condition?: { revision: Revision }; signal?: AbortSignal },
139
+ ): Promise<void>;
140
+ }
141
+
142
+ /** A visitor signed in to this site with their Naru account. */
143
+ export interface Member extends SiteCode, LiveQueries {
144
+ /**
145
+ * This member's ID on this site, the same on every visit: `ctx.member.id`
146
+ * in the site's queries and mutations. It is not their Naru account name.
147
+ */
148
+ readonly id: string;
149
+ media: {
150
+ /**
151
+ * Stores a file publicly, in the site's media. Large photos may be shrunk
152
+ * first. A HEIC photo this browser cannot convert throws a `TypeError`
153
+ * before anything is sent.
154
+ */
155
+ upload(
156
+ file: File | Blob,
157
+ options?: {
158
+ signal?: AbortSignal;
159
+ /** As for an owner's upload. */
160
+ onProgress?: (
161
+ progress:
162
+ | { phase: "preparing" }
163
+ | { phase: "uploading"; loaded: number; total: number }
164
+ | { phase: "finishing" },
165
+ ) => void;
166
+ },
167
+ ): Promise<{
168
+ id: string;
169
+ url: string;
170
+ name: string;
171
+ contentType: string;
172
+ size: number;
173
+ }>;
174
+ };
175
+ /**
176
+ * Signs out on every page of this site. This handle stops working at once,
177
+ * even if the revoke request fails.
178
+ */
179
+ signOut(): Promise<void>;
180
+ }
181
+
182
+ /**
183
+ * The site's queries and mutations: its own code over its own tables, which its `_naru/migrations/` create. Anyone may call them,
184
+ * signed in or not; the code decides what each caller may see and do from
185
+ * `ctx.member` and `ctx.owner`.
186
+ */
187
+ export interface SiteCode {
188
+ /**
189
+ * Runs `"<module/path>:<name>"`, an export of the site's
190
+ * `_naru/queries/<module/path>.js`, and resolves with what it returned. A
191
+ * query only reads (`ctx.sql`), so it counts against the caller's read
192
+ * budget.
193
+ */
194
+ query<R = Json>(
195
+ name: string,
196
+ args?: Json,
197
+ options?: { signal?: AbortSignal },
198
+ ): Promise<R>;
199
+ /**
200
+ * Runs an export of `_naru/mutations/<module/path>.js` in one transaction:
201
+ * what it wrote is kept only if it returns, and a throw, `ctx.fail` or a
202
+ * limit changes nothing.
203
+ */
204
+ mutate<R = Json>(
205
+ name: string,
206
+ args?: Json,
207
+ options?: { signal?: AbortSignal },
208
+ ): Promise<R>;
209
+ }
210
+
211
+ /**
212
+ * The owner signed in on this page. Its queries and mutations see
213
+ * `ctx.member` null and `ctx.owner` true, whatever collections this sign-in
214
+ * approved, and are not counted against the site's budget.
215
+ */
216
+ export interface Admin extends SiteCode, LiveQueries {
217
+ collection<T = Json>(name: string): AdminCollection<T>;
218
+ /**
219
+ * Commits every write or none. Conditions guard individual documents. Does
220
+ * not retry. Resolves, in the order written, with what each `set` stored and
221
+ * `null` for each `delete`.
222
+ */
223
+ batch(
224
+ writes: readonly (
225
+ | {
226
+ collection: string;
227
+ set: { id: string; data: unknown; condition?: WriteCondition };
228
+ }
229
+ | {
230
+ collection: string;
231
+ delete: { id: string; condition?: { revision: Revision } };
232
+ }
233
+ )[],
234
+ options?: { signal?: AbortSignal },
235
+ ): Promise<
236
+ ({
237
+ id: string;
238
+ revision: Revision;
239
+ createdAt: string;
240
+ updatedAt: string;
241
+ } | null)[]
242
+ >;
243
+ media: {
244
+ /**
245
+ * Stores a file publicly. Large photos may be shrunk first. A HEIC photo
246
+ * this browser cannot convert throws a `TypeError` before anything is sent.
247
+ * Like every upload, it stays until the owner deletes it in the media
248
+ * library, since media may be linked from anywhere; Naru notes which of the site's documents name its URL
249
+ * (in any field, HTML included), and the media library marks it when
250
+ * none does.
251
+ */
252
+ upload(
253
+ file: File | Blob,
254
+ options?: {
255
+ signal?: AbortSignal;
256
+ /**
257
+ * Called as the upload moves along: `preparing` while a photo is
258
+ * shrunk and the upload authorized, `uploading` with the bytes sent
259
+ * so far of the `total` actually sent (after shrinking), then
260
+ * `finishing` while storage confirms and Naru records the file.
261
+ * `loaded` can reach `total` before storage has answered.
262
+ */
263
+ onProgress?: (
264
+ progress:
265
+ | { phase: "preparing" }
266
+ | { phase: "uploading"; loaded: number; total: number }
267
+ | { phase: "finishing" },
268
+ ) => void;
269
+ },
270
+ ): Promise<{
271
+ id: string;
272
+ url: string;
273
+ name: string;
274
+ contentType: string;
275
+ size: number;
276
+ }>;
277
+ };
278
+ /** This handle stops working at once, even if the revoke request fails. */
279
+ signOut(): Promise<void>;
280
+ }
281
+
282
+ /** A live query, kept current until closed. */
283
+ export interface Live {
284
+ close(): void;
285
+ }
286
+
287
+ /** Live queries: a query's answer, kept current. */
288
+ export interface LiveQueries {
289
+ /**
290
+ * Runs `"<module/path>:<query>"` from the site's `_naru/queries/` as
291
+ * `query()` does, calls `onResult` with its answer, and again whenever a
292
+ * write changes a table it read and the answer with it, until closed.
293
+ * After the connection drops it is asked again, so the latest answer
294
+ * arrives once it is back. A failure, now or later, goes to `onError`.
295
+ * A connection holds at most 20. `args` left `undefined` is `null`, as
296
+ * `query()`'s are.
297
+ */
298
+ subscribe<R = Json>(
299
+ name: string,
300
+ args: Json | undefined,
301
+ onResult: (result: R) => void,
302
+ options?: { onError?(error: NaruError): void },
303
+ ): Live;
304
+ }
305
+
306
+ /**
307
+ * A page's client. Its queries and mutations run as a visitor, signed in to
308
+ * nothing: they see `ctx.member` null and `ctx.owner` false, are limited per
309
+ * visitor address, and count against the site's budget.
310
+ */
311
+ export interface NaruClient extends SiteCode, LiveQueries {
312
+ /** Visitor access, even when signed in. */
313
+ collection<T = Json>(name: string): PublicCollection<T>;
314
+ auth: {
315
+ /**
316
+ * The admin client for this page, or null. A sign-in stays on this page
317
+ * in every tab until it ends. A sign-in that was denied, went stale or
318
+ * could not be exchanged also resolves null.
319
+ */
320
+ session(): Promise<Admin | null>;
321
+ /** Redirects to Naru for approval. */
322
+ signIn(options?: {
323
+ /**
324
+ * The collections this page manages directly. None, for a page that
325
+ * manages the site through its queries and mutations as the owner.
326
+ */
327
+ collections?: readonly string[];
328
+ }): Promise<void>;
329
+ };
330
+ members: {
331
+ /**
332
+ * The member signed in to this site, or null. A sign-in stays on every
333
+ * page and tab of the site until it ends. Finishes a sign-in when Naru has
334
+ * just redirected back; one that was denied or could not be exchanged
335
+ * resolves null.
336
+ */
337
+ session(): Promise<Member | null>;
338
+ /**
339
+ * Redirects to Naru, where the visitor signs in with a Naru account (or
340
+ * makes one), and back to this page.
341
+ */
342
+ signIn(): Promise<void>;
343
+ };
344
+ }
345
+
346
+ /** Creates a client bound to one site. */
347
+ export declare function createNaru(options?: { site?: string }): NaruClient;
348
+
349
+ export {};