@naru-pub/sdk 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.
- package/LICENSE +661 -0
- package/README.md +81 -2
- package/dist/functions.d.ts +206 -0
- package/dist/naru.d.ts +349 -0
- package/dist/naru.js +1066 -0
- package/package.json +44 -4
package/README.md
CHANGED
|
@@ -1,3 +1,82 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @naru-pub/sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The [Naru](https://naru.pub) Data SDK as an npm package: a Naru site's
|
|
4
|
+
collections, its own queries and mutations, live queries and members, from
|
|
5
|
+
its pages; and, under `@naru-pub/sdk/functions`, the types of the site's own
|
|
6
|
+
code in `_naru/`. The [reference](https://naru.pub/docs/sdk/1.1.0) and the
|
|
7
|
+
guides on naru.pub cover what it does.
|
|
8
|
+
|
|
9
|
+
The package is the very file Naru serves at
|
|
10
|
+
`https://sdk.naru.pub/sdk/<version>/naru.js`, with its declarations. A page
|
|
11
|
+
without a build step imports that address and needs nothing from here:
|
|
12
|
+
|
|
13
|
+
```html
|
|
14
|
+
<script type="module">
|
|
15
|
+
import { createNaru } from "https://sdk.naru.pub/sdk/1.1.0/naru.js";
|
|
16
|
+
</script>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
A project with a bundler, a type checker or a Node program installs it:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @naru-pub/sdk
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { createNaru, NaruError } from "@naru-pub/sdk";
|
|
27
|
+
import type { Query, Mutation } from "@naru-pub/sdk/functions";
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Outside a page on `<site>.naru.pub`, say which site:
|
|
31
|
+
`createNaru({ site: "demo" })`. Node needs version 22 or later for live
|
|
32
|
+
queries (its built-in WebSocket).
|
|
33
|
+
|
|
34
|
+
## Bundled or loaded from Naru
|
|
35
|
+
|
|
36
|
+
Bundled, a site keeps the SDK it was built with until it is built again.
|
|
37
|
+
Loaded from `sdk.naru.pub`, at `/sdk/1/` it gets the SDK's fixes as Naru serves them, and either way
|
|
38
|
+
[naru-dev](https://www.npmjs.com/package/@naru-pub/dev) can point it at your machine. To keep that
|
|
39
|
+
while importing from the package, mark it external and give it the address
|
|
40
|
+
of the installed version, for example in Vite:
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
import { readFileSync } from "node:fs";
|
|
44
|
+
|
|
45
|
+
const { version } = JSON.parse(
|
|
46
|
+
readFileSync("node_modules/@naru-pub/sdk/package.json", "utf8"),
|
|
47
|
+
);
|
|
48
|
+
const sdk = `https://sdk.naru.pub/sdk/${version.replace(/-.*/, "")}/naru.js`;
|
|
49
|
+
|
|
50
|
+
export default {
|
|
51
|
+
plugins: [
|
|
52
|
+
{
|
|
53
|
+
name: "naru-sdk",
|
|
54
|
+
enforce: "pre",
|
|
55
|
+
resolveId: (id) =>
|
|
56
|
+
id === "@naru-pub/sdk" ? { id: sdk, external: true } : null,
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Versions
|
|
63
|
+
|
|
64
|
+
The package's version is the SDK's: `1.1.0` is `/sdk/1.1.0/naru.js`. While
|
|
65
|
+
an SDK version is still being built, the file Naru serves at its address
|
|
66
|
+
can change and a published package cannot, so it is published as
|
|
67
|
+
prereleases, `<version>-dev.1` and on, under the dist-tag `dev`.
|
|
68
|
+
|
|
69
|
+
## Publishing
|
|
70
|
+
|
|
71
|
+
From the Naru repository (maintainers): run `pnpm sdk:build` in
|
|
72
|
+
`control-plane/` if the SDK changed, set `version` in `sdk/package.json`
|
|
73
|
+
(`scripts/build.mjs` refuses a release of an SDK version still being built,
|
|
74
|
+
and a prerelease of a frozen one), commit, and push a tag `sdk-v<version>`;
|
|
75
|
+
`.github/workflows/publish-sdk.yml` checks and stages it with the
|
|
76
|
+
`NPM_TOKEN` secret, and it goes live once approved with 2FA (`npm stage
|
|
77
|
+
approve <stage-id>`, or the Staged Packages tab on npmjs.com).
|
|
78
|
+
`scripts/check-package.sh <dir>` does the same check locally.
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
AGPL-3.0-only, as Naru.
|
|
@@ -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>;
|
package/dist/naru.d.ts
ADDED
|
@@ -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 {};
|