backd-js 0.2.0 → 0.4.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/package.json +1 -1
- package/src/admin.js +12 -6
- package/src/auth.js +23 -7
- package/src/client.js +13 -3
- package/src/data.js +46 -10
- package/src/index.js +1 -0
- package/src/storage.js +7 -0
- package/types/admin.d.ts +20 -5
- package/types/auth.d.ts +13 -4
- package/types/client.d.ts +8 -0
- package/types/data.d.ts +40 -10
- package/types/index.d.ts +2 -0
- package/types/storage.d.ts +6 -0
package/package.json
CHANGED
package/src/admin.js
CHANGED
|
@@ -27,6 +27,7 @@ import { Job } from './functions.js'
|
|
|
27
27
|
* @property {number} limit
|
|
28
28
|
* @property {number} skip
|
|
29
29
|
* @property {boolean} has_more
|
|
30
|
+
* @property {string} [next_cursor] With `has_more`: pass it as `after` for the next page.
|
|
30
31
|
*/
|
|
31
32
|
|
|
32
33
|
/**
|
|
@@ -65,6 +66,7 @@ import { Job } from './functions.js'
|
|
|
65
66
|
* @property {'data' | 'admin'} role
|
|
66
67
|
* @property {string} prefix First characters of the key.
|
|
67
68
|
* @property {string[]} networks Where it may be used from; empty: anywhere.
|
|
69
|
+
* @property {string[]} scopes What it reaches (`read:blog/posts`, `call:main/export`…); empty: everything.
|
|
68
70
|
* @property {string} created_at
|
|
69
71
|
* @property {string | null} last_used_at
|
|
70
72
|
* @property {string | null} expires_at
|
|
@@ -225,13 +227,14 @@ class AdminUsers {
|
|
|
225
227
|
}
|
|
226
228
|
|
|
227
229
|
/**
|
|
228
|
-
* A page of users, sorted by email.
|
|
229
|
-
*
|
|
230
|
+
* A page of users, sorted by email. `after` is the `next_cursor` of the
|
|
231
|
+
* previous page (not combinable with `skip`).
|
|
232
|
+
* @param {{ limit?: number, skip?: number, after?: string }} [params]
|
|
230
233
|
* @param {RequestOptions} [opts]
|
|
231
234
|
* @returns {Promise<UserPage>}
|
|
232
235
|
*/
|
|
233
236
|
async list(params = {}, opts) {
|
|
234
|
-
return (await this.admin._request({ method: 'GET', path: ['users'], query: { limit: params.limit, skip: params.skip }, ...opts })).data
|
|
237
|
+
return (await this.admin._request({ method: 'GET', path: ['users'], query: { limit: params.limit, skip: params.skip, after: params.after }, ...opts })).data
|
|
235
238
|
}
|
|
236
239
|
|
|
237
240
|
/**
|
|
@@ -388,17 +391,20 @@ class AdminAPIKeys {
|
|
|
388
391
|
|
|
389
392
|
/**
|
|
390
393
|
* Creates a key; store its `key` now, it is never shown again.
|
|
391
|
-
* @param {{ name: string, role?: 'data' | 'admin', expiresIn?: string, networks?: string[] }} input
|
|
392
|
-
* `role` defaults to data; `expiresIn`: days (`90d`) or Go durations (`12h`).
|
|
394
|
+
* @param {{ name: string, role?: 'data' | 'admin', expiresIn?: string, networks?: string[], scopes?: string[] }} input
|
|
395
|
+
* `role` defaults to data; `expiresIn`: days (`90d`) or Go durations (`12h`). `scopes` limit what a
|
|
396
|
+
* data key reaches: `read`, `write` or `call`, optionally followed by `:<database>` or
|
|
397
|
+
* `:<database>/<collection or function>`; none means everything.
|
|
393
398
|
* @param {RequestOptions} [opts]
|
|
394
399
|
* @returns {Promise<NewAPIKey>}
|
|
395
400
|
*/
|
|
396
|
-
async create({ name, role, expiresIn, networks }, opts) {
|
|
401
|
+
async create({ name, role, expiresIn, networks, scopes }, opts) {
|
|
397
402
|
/** @type {Record<string, unknown>} */
|
|
398
403
|
const body = { name }
|
|
399
404
|
if (role !== undefined) body.role = role
|
|
400
405
|
if (expiresIn !== undefined) body.expires_in = expiresIn
|
|
401
406
|
if (networks !== undefined) body.networks = networks
|
|
407
|
+
if (scopes !== undefined) body.scopes = scopes
|
|
402
408
|
return (await this.admin._request({ method: 'POST', path: ['apikeys'], body, ...opts })).data
|
|
403
409
|
}
|
|
404
410
|
|
package/src/auth.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AuthenticationError, VerificationRequiredError } from './errors.js'
|
|
2
|
+
import { COOKIE_SESSION } from './storage.js'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* @typedef {import('./client.js').Client} Client
|
|
@@ -19,8 +20,9 @@ import { AuthenticationError, VerificationRequiredError } from './errors.js'
|
|
|
19
20
|
/**
|
|
20
21
|
* A new session.
|
|
21
22
|
* @typedef {object} Session
|
|
22
|
-
* @property {string} token
|
|
23
|
-
*
|
|
23
|
+
* @property {string} [token] Session token (`bds_…`); already stored by the client. Absent with `cookies: true`:
|
|
24
|
+
* the session is in an HttpOnly cookie, out of reach of scripts.
|
|
25
|
+
* @property {'Bearer'} [token_type]
|
|
24
26
|
* @property {string} session_id
|
|
25
27
|
* @property {string} expires_at RFC3339; pushed forward as the session is used.
|
|
26
28
|
* @property {User} user
|
|
@@ -81,8 +83,9 @@ export class Auth {
|
|
|
81
83
|
* @returns {Promise<Session>}
|
|
82
84
|
*/
|
|
83
85
|
async signup({ email, password, invitation, locale, redirectTo }, opts) {
|
|
84
|
-
/** @type {Record<string, string>} */
|
|
86
|
+
/** @type {Record<string, string | boolean>} */
|
|
85
87
|
const body = { email, password }
|
|
88
|
+
if (this.client.cookies) body.cookie = true
|
|
86
89
|
if (invitation) body.invitation = invitation
|
|
87
90
|
if (locale) body.locale = locale
|
|
88
91
|
if (redirectTo) body.redirect_to = redirectTo
|
|
@@ -100,7 +103,8 @@ export class Auth {
|
|
|
100
103
|
* @returns {Promise<Session>}
|
|
101
104
|
*/
|
|
102
105
|
async login({ email, password }, opts) {
|
|
103
|
-
const
|
|
106
|
+
const body = this.client.cookies ? { email, password, cookie: true } : { email, password }
|
|
107
|
+
const { data } = await this.client.request({ method: 'POST', path: ['_auth', 'login'], body, auth: false, ...opts })
|
|
104
108
|
return this.signedIn(data)
|
|
105
109
|
}
|
|
106
110
|
|
|
@@ -311,11 +315,23 @@ export class Auth {
|
|
|
311
315
|
}
|
|
312
316
|
|
|
313
317
|
/**
|
|
314
|
-
* The stored session token, if any.
|
|
318
|
+
* The stored session token, if any. Always null with `cookies: true`: the
|
|
319
|
+
* token is in a cookie that scripts can't read.
|
|
315
320
|
* @returns {Promise<string | null>}
|
|
316
321
|
*/
|
|
317
322
|
async token() {
|
|
318
|
-
|
|
323
|
+
const token = (await this.client.storage.get()) ?? null
|
|
324
|
+
return token === COOKIE_SESSION ? null : token
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Whether this client believes it is signed in: it holds a token, or (with
|
|
329
|
+
* `cookies: true`) it signed in and hasn't signed out or been refused since.
|
|
330
|
+
* A page that has just loaded knows nothing: ask the server with {@link Auth#me}.
|
|
331
|
+
* @returns {Promise<boolean>}
|
|
332
|
+
*/
|
|
333
|
+
async hasSession() {
|
|
334
|
+
return Boolean(await this.client.storage.get())
|
|
319
335
|
}
|
|
320
336
|
|
|
321
337
|
/**
|
|
@@ -334,7 +350,7 @@ export class Auth {
|
|
|
334
350
|
* @returns {Promise<Session>}
|
|
335
351
|
*/
|
|
336
352
|
async signedIn(session) {
|
|
337
|
-
await this.client.storage.set(session.token)
|
|
353
|
+
await this.client.storage.set(session.token ?? COOKIE_SESSION)
|
|
338
354
|
this.emit('SIGNED_IN', session)
|
|
339
355
|
return session
|
|
340
356
|
}
|
package/src/client.js
CHANGED
|
@@ -2,7 +2,7 @@ import { Admin } from './admin.js'
|
|
|
2
2
|
import { Auth } from './auth.js'
|
|
3
3
|
import { Database } from './data.js'
|
|
4
4
|
import { NetworkError, RetryableError, errorFromResponse } from './errors.js'
|
|
5
|
-
import { memoryStorage } from './storage.js'
|
|
5
|
+
import { COOKIE_SESSION, memoryStorage } from './storage.js'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* @typedef {import('./storage.js').TokenStorage} TokenStorage
|
|
@@ -23,6 +23,9 @@ import { memoryStorage } from './storage.js'
|
|
|
23
23
|
* @property {string} [apiKey] Server-side only: an API key (`bdk_…`). Full access to the realm.
|
|
24
24
|
* @property {boolean} [dangerouslyAllowBrowser] Allow `apiKey` in a browser. Anyone who loads the page gets the key.
|
|
25
25
|
* @property {TokenStorage} [storage] Where the session token lives; memory by default.
|
|
26
|
+
* @property {boolean} [cookies] Browser apps: keep the session in an HttpOnly cookie that scripts can't read,
|
|
27
|
+
* instead of a token. The realm must enable `sessions.cookie` and list the app's origin in `cors.origins`; the
|
|
28
|
+
* app's page (or the API) must be on a site the cookie's SameSite setting allows. The admin API doesn't take cookies.
|
|
26
29
|
* @property {RetryOptions} [retry] Retries for 429/503; off by default.
|
|
27
30
|
* @property {typeof fetch} [fetch] A fetch implementation; the global one by default.
|
|
28
31
|
* @property {Record<string, string>} [headers] Extra headers for every request.
|
|
@@ -81,11 +84,16 @@ export class Client {
|
|
|
81
84
|
'`dangerouslyAllowBrowser: true` if you really mean it.',
|
|
82
85
|
)
|
|
83
86
|
}
|
|
87
|
+
if (options.cookies && options.apiKey) {
|
|
88
|
+
throw new TypeError('createClient: `cookies` is for sessions in a browser; an API key is sent in a header')
|
|
89
|
+
}
|
|
84
90
|
/** @readonly */
|
|
85
91
|
this.url = options.url.replace(/\/+$/, '')
|
|
86
92
|
/** @readonly */
|
|
87
93
|
this.realm = options.realm
|
|
88
94
|
/** @internal */
|
|
95
|
+
this.cookies = Boolean(options.cookies)
|
|
96
|
+
/** @internal */
|
|
89
97
|
this.apiKey = options.apiKey
|
|
90
98
|
/** @internal */
|
|
91
99
|
this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis)
|
|
@@ -138,14 +146,15 @@ export class Client {
|
|
|
138
146
|
} else {
|
|
139
147
|
const token = await this.storage.get()
|
|
140
148
|
if (token) {
|
|
141
|
-
headers.Authorization = 'Bearer ' + token
|
|
149
|
+
if (token !== COOKIE_SESSION) headers.Authorization = 'Bearer ' + token // a cookie session is sent by the browser
|
|
142
150
|
sentSession = true
|
|
143
151
|
}
|
|
144
152
|
}
|
|
145
153
|
}
|
|
146
154
|
|
|
147
155
|
const retry = req.retry ?? this.retry
|
|
148
|
-
|
|
156
|
+
// Safe to repeat: reads, writes conditional on a version, and requests the server deduplicates by key.
|
|
157
|
+
const safe = req.method === 'GET' || Object.keys(headers).some((h) => ['if-match', 'idempotency-key'].includes(h.toLowerCase()))
|
|
149
158
|
const attempts = safe ? Math.max(0, retry.attempts) : 0
|
|
150
159
|
for (let attempt = 0; ; attempt++) {
|
|
151
160
|
/** @type {Response} */
|
|
@@ -156,6 +165,7 @@ export class Client {
|
|
|
156
165
|
headers,
|
|
157
166
|
body: req.body === undefined ? undefined : JSON.stringify(req.body),
|
|
158
167
|
signal: req.signal,
|
|
168
|
+
credentials: this.cookies ? 'include' : undefined,
|
|
159
169
|
})
|
|
160
170
|
} catch (cause) {
|
|
161
171
|
throw new NetworkError({
|
package/src/data.js
CHANGED
|
@@ -30,6 +30,8 @@ import { Job } from './functions.js'
|
|
|
30
30
|
* @property {string | string[]} [orderBy] Fields, `-` prefix for descending, e.g. `'-_meta.created_at'`.
|
|
31
31
|
* @property {number} [limit] 1–100; default 20.
|
|
32
32
|
* @property {number} [skip]
|
|
33
|
+
* @property {string} [after] The `next_cursor` of the previous page: continues the list after it
|
|
34
|
+
* (same `where` and `orderBy`). Can't be combined with `skip`.
|
|
33
35
|
* @property {boolean} [count] Also return `total`.
|
|
34
36
|
*/
|
|
35
37
|
|
|
@@ -41,7 +43,9 @@ import { Job } from './functions.js'
|
|
|
41
43
|
* @property {number} limit
|
|
42
44
|
* @property {number} skip
|
|
43
45
|
* @property {boolean} has_more
|
|
44
|
-
* @property {
|
|
46
|
+
* @property {string} [next_cursor] With `has_more`: pass it as `after` to get the next page. Absent when the
|
|
47
|
+
* `orderBy` can't be followed by a cursor (arrays, objects, mixed types): use `skip` then.
|
|
48
|
+
* @property {number} [total] With `count: true`; counts the whole list, not what is after `after`.
|
|
45
49
|
*/
|
|
46
50
|
|
|
47
51
|
/**
|
|
@@ -50,6 +54,15 @@ import { Job } from './functions.js'
|
|
|
50
54
|
* @typedef {RequestOptions & { ifMatch?: number | string }} WriteOptions
|
|
51
55
|
*/
|
|
52
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Options for creates and batches. `idempotencyKey` makes a retry safe: the
|
|
59
|
+
* server remembers the answer of the first request with a key for 24 hours and
|
|
60
|
+
* returns it again for the same key and body, instead of creating a second
|
|
61
|
+
* document. The client also retries such a request itself after a network
|
|
62
|
+
* error, `429` or `503` (see `retry`).
|
|
63
|
+
* @typedef {RequestOptions & { idempotencyKey?: string }} CreateOptions
|
|
64
|
+
*/
|
|
65
|
+
|
|
53
66
|
/**
|
|
54
67
|
* One write for `db.batch(...)`, applied atomically with the others.
|
|
55
68
|
* `create` and `replace` need `document`; `patch` needs `patch` (a JSON
|
|
@@ -95,14 +108,14 @@ export class Database {
|
|
|
95
108
|
* mismatch (`ifMatch`) is a `VersionMismatchError`, same as a single
|
|
96
109
|
* write.
|
|
97
110
|
* @param {BatchOperation[]} operations
|
|
98
|
-
* @param {
|
|
111
|
+
* @param {CreateOptions} [opts]
|
|
99
112
|
* @returns {Promise<(Doc | { id: string })[]>} one entry per operation, in order
|
|
100
113
|
*/
|
|
101
114
|
async batch(operations, opts) {
|
|
102
115
|
const body = {
|
|
103
116
|
operations: operations.map(({ ifMatch, ...op }) => (ifMatch === undefined ? op : { ...op, if_match: ifMatchValue(ifMatch) })),
|
|
104
117
|
}
|
|
105
|
-
const { data } = await this.client.request({ method: 'POST', path: [this.name, '_batch'], body, ...opts })
|
|
118
|
+
const { data } = await this.client.request({ method: 'POST', path: [this.name, '_batch'], body, ...keyed(opts) })
|
|
106
119
|
return data.results
|
|
107
120
|
}
|
|
108
121
|
|
|
@@ -167,19 +180,29 @@ export class Collection {
|
|
|
167
180
|
|
|
168
181
|
/**
|
|
169
182
|
* Every matching document, fetching pages as needed:
|
|
170
|
-
* `for await (const doc of posts.iterate({ where }))`. Pages
|
|
171
|
-
*
|
|
172
|
-
*
|
|
183
|
+
* `for await (const doc of posts.iterate({ where }))`. Pages follow a
|
|
184
|
+
* cursor (`next_cursor`), so documents created, changed or deleted
|
|
185
|
+
* meanwhile never shift a page, and it is as fast at the end as at the
|
|
186
|
+
* start. An `orderBy` on arrays, objects or mixed types has no cursor:
|
|
187
|
+
* those lists are fetched by offset instead, where documents created or
|
|
188
|
+
* deleted meanwhile can be skipped or repeated.
|
|
173
189
|
* @param {Omit<ListParams, 'skip' | 'count'>} [params] `limit` is the page size (default 100).
|
|
174
190
|
* @param {RequestOptions} [opts]
|
|
175
191
|
* @returns {AsyncGenerator<Doc<T>, void, undefined>}
|
|
176
192
|
*/
|
|
177
193
|
async *iterate(params = {}, opts) {
|
|
178
194
|
const limit = params.limit ?? 100
|
|
195
|
+
let after = params.after
|
|
179
196
|
for (let skip = 0; ; skip += limit) {
|
|
180
|
-
|
|
197
|
+
// With a cursor the next request carries it; without one, the offset.
|
|
198
|
+
const page = await this.list(after === undefined ? { ...params, limit, skip } : { ...params, limit, after }, opts)
|
|
181
199
|
yield* page.items
|
|
182
200
|
if (!page.has_more) return
|
|
201
|
+
if (page.next_cursor === undefined) {
|
|
202
|
+
if (after !== undefined) throw new Error('backd: the list has no next_cursor to continue after')
|
|
203
|
+
} else {
|
|
204
|
+
after = page.next_cursor
|
|
205
|
+
}
|
|
183
206
|
}
|
|
184
207
|
}
|
|
185
208
|
|
|
@@ -194,13 +217,15 @@ export class Collection {
|
|
|
194
217
|
}
|
|
195
218
|
|
|
196
219
|
/**
|
|
197
|
-
* Creates a document. `id` and `_meta` are set by the server.
|
|
220
|
+
* Creates a document. `id` and `_meta` are set by the server. With an
|
|
221
|
+
* `idempotencyKey`, a repeated request returns the document created by the
|
|
222
|
+
* first one.
|
|
198
223
|
* @param {T} doc
|
|
199
|
-
* @param {
|
|
224
|
+
* @param {CreateOptions} [opts]
|
|
200
225
|
* @returns {Promise<Doc<T>>}
|
|
201
226
|
*/
|
|
202
227
|
async create(doc, opts) {
|
|
203
|
-
return (await this.client.request({ method: 'POST', path: this.path, body: doc, ...opts })).data
|
|
228
|
+
return (await this.client.request({ method: 'POST', path: this.path, body: doc, ...keyed(opts) })).data
|
|
204
229
|
}
|
|
205
230
|
|
|
206
231
|
/**
|
|
@@ -255,10 +280,21 @@ function listQuery(p) {
|
|
|
255
280
|
order_by: Array.isArray(p.orderBy) ? p.orderBy.join(',') : p.orderBy,
|
|
256
281
|
limit: p.limit,
|
|
257
282
|
skip: p.skip,
|
|
283
|
+
after: p.after,
|
|
258
284
|
count: p.count || undefined,
|
|
259
285
|
}
|
|
260
286
|
}
|
|
261
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Turns `idempotencyKey` into an Idempotency-Key header.
|
|
290
|
+
* @param {CreateOptions} [opts]
|
|
291
|
+
* @returns {RequestOptions}
|
|
292
|
+
*/
|
|
293
|
+
function keyed(opts = {}) {
|
|
294
|
+
const { idempotencyKey, ...rest } = opts
|
|
295
|
+
return idempotencyKey === undefined ? rest : { ...rest, headers: { ...rest.headers, 'Idempotency-Key': idempotencyKey } }
|
|
296
|
+
}
|
|
297
|
+
|
|
262
298
|
/**
|
|
263
299
|
* Turns `ifMatch` into an If-Match header.
|
|
264
300
|
* @param {WriteOptions} opts
|
package/src/index.js
CHANGED
|
@@ -30,6 +30,7 @@ export { memoryStorage, localStorageStorage } from './storage.js'
|
|
|
30
30
|
* @typedef {import('./data.js').Meta} Meta
|
|
31
31
|
* @typedef {import('./data.js').ListParams} ListParams
|
|
32
32
|
* @typedef {import('./data.js').WriteOptions} WriteOptions
|
|
33
|
+
* @typedef {import('./data.js').CreateOptions} CreateOptions
|
|
33
34
|
* @typedef {import('./admin.js').AdminUser} AdminUser
|
|
34
35
|
* @typedef {import('./admin.js').UserPage} UserPage
|
|
35
36
|
* @typedef {import('./admin.js').Invitation} Invitation
|
package/src/storage.js
CHANGED
|
@@ -6,6 +6,13 @@
|
|
|
6
6
|
* @property {() => void | Promise<void>} remove
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* What the client stores, instead of a token, when the session lives in an
|
|
11
|
+
* HttpOnly cookie (`cookies: true`): a hint that there is a session, with
|
|
12
|
+
* nothing secret in it.
|
|
13
|
+
*/
|
|
14
|
+
export const COOKIE_SESSION = 'cookie-session'
|
|
15
|
+
|
|
9
16
|
/**
|
|
10
17
|
* Keeps the token in memory: it is lost on reload, and scripts on the page
|
|
11
18
|
* can't read it from storage. The default.
|
package/types/admin.d.ts
CHANGED
|
@@ -28,6 +28,10 @@ export type UserPage = {
|
|
|
28
28
|
limit: number;
|
|
29
29
|
skip: number;
|
|
30
30
|
has_more: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* With `has_more`: pass it as `after` for the next page.
|
|
33
|
+
*/
|
|
34
|
+
next_cursor?: string;
|
|
31
35
|
};
|
|
32
36
|
export type Invitation = {
|
|
33
37
|
id: string;
|
|
@@ -79,6 +83,10 @@ export type APIKeyInfo = {
|
|
|
79
83
|
* Where it may be used from; empty: anywhere.
|
|
80
84
|
*/
|
|
81
85
|
networks: string[];
|
|
86
|
+
/**
|
|
87
|
+
* What it reaches (`read:blog/posts`, `call:main/export`…); empty: everything.
|
|
88
|
+
*/
|
|
89
|
+
scopes: string[];
|
|
82
90
|
created_at: string;
|
|
83
91
|
last_used_at: string | null;
|
|
84
92
|
expires_at: string | null;
|
|
@@ -241,6 +249,7 @@ export type JobsPage = {
|
|
|
241
249
|
* @property {number} limit
|
|
242
250
|
* @property {number} skip
|
|
243
251
|
* @property {boolean} has_more
|
|
252
|
+
* @property {string} [next_cursor] With `has_more`: pass it as `after` for the next page.
|
|
244
253
|
*/
|
|
245
254
|
/**
|
|
246
255
|
* @typedef {object} Invitation
|
|
@@ -274,6 +283,7 @@ export type JobsPage = {
|
|
|
274
283
|
* @property {'data' | 'admin'} role
|
|
275
284
|
* @property {string} prefix First characters of the key.
|
|
276
285
|
* @property {string[]} networks Where it may be used from; empty: anywhere.
|
|
286
|
+
* @property {string[]} scopes What it reaches (`read:blog/posts`, `call:main/export`…); empty: everything.
|
|
277
287
|
* @property {string} created_at
|
|
278
288
|
* @property {string | null} last_used_at
|
|
279
289
|
* @property {string | null} expires_at
|
|
@@ -408,14 +418,16 @@ declare class AdminUsers {
|
|
|
408
418
|
/** @param {Admin} admin */
|
|
409
419
|
constructor(admin: Admin);
|
|
410
420
|
/**
|
|
411
|
-
* A page of users, sorted by email.
|
|
412
|
-
*
|
|
421
|
+
* A page of users, sorted by email. `after` is the `next_cursor` of the
|
|
422
|
+
* previous page (not combinable with `skip`).
|
|
423
|
+
* @param {{ limit?: number, skip?: number, after?: string }} [params]
|
|
413
424
|
* @param {RequestOptions} [opts]
|
|
414
425
|
* @returns {Promise<UserPage>}
|
|
415
426
|
*/
|
|
416
427
|
list(params?: {
|
|
417
428
|
limit?: number;
|
|
418
429
|
skip?: number;
|
|
430
|
+
after?: string;
|
|
419
431
|
}, opts?: RequestOptions): Promise<UserPage>;
|
|
420
432
|
/**
|
|
421
433
|
* The user with this email (case-insensitive), or null.
|
|
@@ -536,16 +548,19 @@ declare class AdminAPIKeys {
|
|
|
536
548
|
list(opts?: RequestOptions): Promise<APIKeyInfo[]>;
|
|
537
549
|
/**
|
|
538
550
|
* Creates a key; store its `key` now, it is never shown again.
|
|
539
|
-
* @param {{ name: string, role?: 'data' | 'admin', expiresIn?: string, networks?: string[] }} input
|
|
540
|
-
* `role` defaults to data; `expiresIn`: days (`90d`) or Go durations (`12h`).
|
|
551
|
+
* @param {{ name: string, role?: 'data' | 'admin', expiresIn?: string, networks?: string[], scopes?: string[] }} input
|
|
552
|
+
* `role` defaults to data; `expiresIn`: days (`90d`) or Go durations (`12h`). `scopes` limit what a
|
|
553
|
+
* data key reaches: `read`, `write` or `call`, optionally followed by `:<database>` or
|
|
554
|
+
* `:<database>/<collection or function>`; none means everything.
|
|
541
555
|
* @param {RequestOptions} [opts]
|
|
542
556
|
* @returns {Promise<NewAPIKey>}
|
|
543
557
|
*/
|
|
544
|
-
create({ name, role, expiresIn, networks }: {
|
|
558
|
+
create({ name, role, expiresIn, networks, scopes }: {
|
|
545
559
|
name: string;
|
|
546
560
|
role?: 'data' | 'admin';
|
|
547
561
|
expiresIn?: string;
|
|
548
562
|
networks?: string[];
|
|
563
|
+
scopes?: string[];
|
|
549
564
|
}, opts?: RequestOptions): Promise<NewAPIKey>;
|
|
550
565
|
/**
|
|
551
566
|
* Revokes a key: it stops working at once.
|
package/types/auth.d.ts
CHANGED
|
@@ -16,10 +16,11 @@ export type User = {
|
|
|
16
16
|
};
|
|
17
17
|
export type Session = {
|
|
18
18
|
/**
|
|
19
|
-
* Session token (`bds_…`); already stored by the client.
|
|
19
|
+
* Session token (`bds_…`); already stored by the client. Absent with `cookies: true`:
|
|
20
|
+
* the session is in an HttpOnly cookie, out of reach of scripts.
|
|
20
21
|
*/
|
|
21
|
-
token
|
|
22
|
-
token_type
|
|
22
|
+
token?: string;
|
|
23
|
+
token_type?: 'Bearer';
|
|
23
24
|
session_id: string;
|
|
24
25
|
/**
|
|
25
26
|
* RFC3339; pushed forward as the session is used.
|
|
@@ -246,10 +247,18 @@ export declare class Auth {
|
|
|
246
247
|
*/
|
|
247
248
|
revokeSession(id: string, opts?: RequestOptions): Promise<void>;
|
|
248
249
|
/**
|
|
249
|
-
* The stored session token, if any.
|
|
250
|
+
* The stored session token, if any. Always null with `cookies: true`: the
|
|
251
|
+
* token is in a cookie that scripts can't read.
|
|
250
252
|
* @returns {Promise<string | null>}
|
|
251
253
|
*/
|
|
252
254
|
token(): Promise<string | null>;
|
|
255
|
+
/**
|
|
256
|
+
* Whether this client believes it is signed in: it holds a token, or (with
|
|
257
|
+
* `cookies: true`) it signed in and hasn't signed out or been refused since.
|
|
258
|
+
* A page that has just loaded knows nothing: ask the server with {@link Auth#me}.
|
|
259
|
+
* @returns {Promise<boolean>}
|
|
260
|
+
*/
|
|
261
|
+
hasSession(): Promise<boolean>;
|
|
253
262
|
/**
|
|
254
263
|
* Calls listener on sign-in, sign-out and session expiry.
|
|
255
264
|
* @param {AuthListener} listener
|
package/types/client.d.ts
CHANGED
|
@@ -33,6 +33,12 @@ export type ClientOptions = {
|
|
|
33
33
|
* Where the session token lives; memory by default.
|
|
34
34
|
*/
|
|
35
35
|
storage?: TokenStorage;
|
|
36
|
+
/**
|
|
37
|
+
* Browser apps: keep the session in an HttpOnly cookie that scripts can't read,
|
|
38
|
+
* instead of a token. The realm must enable `sessions.cookie` and list the app's origin in `cors.origins`; the
|
|
39
|
+
* app's page (or the API) must be on a site the cookie's SameSite setting allows. The admin API doesn't take cookies.
|
|
40
|
+
*/
|
|
41
|
+
cookies?: boolean;
|
|
36
42
|
/**
|
|
37
43
|
* Retries for 429/503; off by default.
|
|
38
44
|
*/
|
|
@@ -98,6 +104,8 @@ export declare class Client {
|
|
|
98
104
|
/** @readonly */
|
|
99
105
|
realm: string;
|
|
100
106
|
/** @internal */
|
|
107
|
+
cookies: boolean;
|
|
108
|
+
/** @internal */
|
|
101
109
|
apiKey: string | undefined;
|
|
102
110
|
/** @internal */
|
|
103
111
|
fetchImpl: typeof fetch;
|
package/types/data.d.ts
CHANGED
|
@@ -47,6 +47,11 @@ export type ListParams = {
|
|
|
47
47
|
*/
|
|
48
48
|
limit?: number;
|
|
49
49
|
skip?: number;
|
|
50
|
+
/**
|
|
51
|
+
* The `next_cursor` of the previous page: continues the list after it
|
|
52
|
+
* (same `where` and `orderBy`). Can't be combined with `skip`.
|
|
53
|
+
*/
|
|
54
|
+
after?: string;
|
|
50
55
|
/**
|
|
51
56
|
* Also return `total`.
|
|
52
57
|
*/
|
|
@@ -58,13 +63,21 @@ export type Page<T extends object = Record<string, any>> = {
|
|
|
58
63
|
skip: number;
|
|
59
64
|
has_more: boolean;
|
|
60
65
|
/**
|
|
61
|
-
* With `
|
|
66
|
+
* With `has_more`: pass it as `after` to get the next page. Absent when the
|
|
67
|
+
* `orderBy` can't be followed by a cursor (arrays, objects, mixed types): use `skip` then.
|
|
68
|
+
*/
|
|
69
|
+
next_cursor?: string;
|
|
70
|
+
/**
|
|
71
|
+
* With `count: true`; counts the whole list, not what is after `after`.
|
|
62
72
|
*/
|
|
63
73
|
total?: number;
|
|
64
74
|
};
|
|
65
75
|
export type WriteOptions = RequestOptions & {
|
|
66
76
|
ifMatch?: number | string;
|
|
67
77
|
};
|
|
78
|
+
export type CreateOptions = RequestOptions & {
|
|
79
|
+
idempotencyKey?: string;
|
|
80
|
+
};
|
|
68
81
|
export type BatchOperation = {
|
|
69
82
|
op: 'create' | 'replace' | 'patch' | 'delete';
|
|
70
83
|
collection: string;
|
|
@@ -100,6 +113,8 @@ export type BatchOperation = {
|
|
|
100
113
|
* @property {string | string[]} [orderBy] Fields, `-` prefix for descending, e.g. `'-_meta.created_at'`.
|
|
101
114
|
* @property {number} [limit] 1–100; default 20.
|
|
102
115
|
* @property {number} [skip]
|
|
116
|
+
* @property {string} [after] The `next_cursor` of the previous page: continues the list after it
|
|
117
|
+
* (same `where` and `orderBy`). Can't be combined with `skip`.
|
|
103
118
|
* @property {boolean} [count] Also return `total`.
|
|
104
119
|
*/
|
|
105
120
|
/**
|
|
@@ -110,13 +125,23 @@ export type BatchOperation = {
|
|
|
110
125
|
* @property {number} limit
|
|
111
126
|
* @property {number} skip
|
|
112
127
|
* @property {boolean} has_more
|
|
113
|
-
* @property {
|
|
128
|
+
* @property {string} [next_cursor] With `has_more`: pass it as `after` to get the next page. Absent when the
|
|
129
|
+
* `orderBy` can't be followed by a cursor (arrays, objects, mixed types): use `skip` then.
|
|
130
|
+
* @property {number} [total] With `count: true`; counts the whole list, not what is after `after`.
|
|
114
131
|
*/
|
|
115
132
|
/**
|
|
116
133
|
* Options for writes. `ifMatch` makes the write fail with a
|
|
117
134
|
* VersionMismatchError unless the document is still at that version.
|
|
118
135
|
* @typedef {RequestOptions & { ifMatch?: number | string }} WriteOptions
|
|
119
136
|
*/
|
|
137
|
+
/**
|
|
138
|
+
* Options for creates and batches. `idempotencyKey` makes a retry safe: the
|
|
139
|
+
* server remembers the answer of the first request with a key for 24 hours and
|
|
140
|
+
* returns it again for the same key and body, instead of creating a second
|
|
141
|
+
* document. The client also retries such a request itself after a network
|
|
142
|
+
* error, `429` or `503` (see `retry`).
|
|
143
|
+
* @typedef {RequestOptions & { idempotencyKey?: string }} CreateOptions
|
|
144
|
+
*/
|
|
120
145
|
/**
|
|
121
146
|
* One write for `db.batch(...)`, applied atomically with the others.
|
|
122
147
|
* `create` and `replace` need `document`; `patch` needs `patch` (a JSON
|
|
@@ -156,10 +181,10 @@ export declare class Database {
|
|
|
156
181
|
* mismatch (`ifMatch`) is a `VersionMismatchError`, same as a single
|
|
157
182
|
* write.
|
|
158
183
|
* @param {BatchOperation[]} operations
|
|
159
|
-
* @param {
|
|
184
|
+
* @param {CreateOptions} [opts]
|
|
160
185
|
* @returns {Promise<(Doc | { id: string })[]>} one entry per operation, in order
|
|
161
186
|
*/
|
|
162
|
-
batch(operations: BatchOperation[], opts?:
|
|
187
|
+
batch(operations: BatchOperation[], opts?: CreateOptions): Promise<(Doc | {
|
|
163
188
|
id: string;
|
|
164
189
|
})[]>;
|
|
165
190
|
/**
|
|
@@ -206,9 +231,12 @@ export declare class Collection<T extends object = Record<string, any>> {
|
|
|
206
231
|
list(params?: ListParams, opts?: RequestOptions): Promise<Page<T>>;
|
|
207
232
|
/**
|
|
208
233
|
* Every matching document, fetching pages as needed:
|
|
209
|
-
* `for await (const doc of posts.iterate({ where }))`. Pages
|
|
210
|
-
*
|
|
211
|
-
*
|
|
234
|
+
* `for await (const doc of posts.iterate({ where }))`. Pages follow a
|
|
235
|
+
* cursor (`next_cursor`), so documents created, changed or deleted
|
|
236
|
+
* meanwhile never shift a page, and it is as fast at the end as at the
|
|
237
|
+
* start. An `orderBy` on arrays, objects or mixed types has no cursor:
|
|
238
|
+
* those lists are fetched by offset instead, where documents created or
|
|
239
|
+
* deleted meanwhile can be skipped or repeated.
|
|
212
240
|
* @param {Omit<ListParams, 'skip' | 'count'>} [params] `limit` is the page size (default 100).
|
|
213
241
|
* @param {RequestOptions} [opts]
|
|
214
242
|
* @returns {AsyncGenerator<Doc<T>, void, undefined>}
|
|
@@ -222,12 +250,14 @@ export declare class Collection<T extends object = Record<string, any>> {
|
|
|
222
250
|
*/
|
|
223
251
|
get(id: string, opts?: RequestOptions): Promise<Doc<T>>;
|
|
224
252
|
/**
|
|
225
|
-
* Creates a document. `id` and `_meta` are set by the server.
|
|
253
|
+
* Creates a document. `id` and `_meta` are set by the server. With an
|
|
254
|
+
* `idempotencyKey`, a repeated request returns the document created by the
|
|
255
|
+
* first one.
|
|
226
256
|
* @param {T} doc
|
|
227
|
-
* @param {
|
|
257
|
+
* @param {CreateOptions} [opts]
|
|
228
258
|
* @returns {Promise<Doc<T>>}
|
|
229
259
|
*/
|
|
230
|
-
create(doc: T, opts?:
|
|
260
|
+
create(doc: T, opts?: CreateOptions): Promise<Doc<T>>;
|
|
231
261
|
/**
|
|
232
262
|
* Replaces the whole document: fields not in `doc` are removed.
|
|
233
263
|
* @param {string} id
|
package/types/index.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export type ErrorDetail = import('./errors.js').ErrorDetail;
|
|
|
17
17
|
export type Meta = import('./data.js').Meta;
|
|
18
18
|
export type ListParams = import('./data.js').ListParams;
|
|
19
19
|
export type WriteOptions = import('./data.js').WriteOptions;
|
|
20
|
+
export type CreateOptions = import('./data.js').CreateOptions;
|
|
20
21
|
export type AdminUser = import('./admin.js').AdminUser;
|
|
21
22
|
export type UserPage = import('./admin.js').UserPage;
|
|
22
23
|
export type Invitation = import('./admin.js').Invitation;
|
|
@@ -40,6 +41,7 @@ export type Page<T extends object = Record<string, any>> = import('./data.js').P
|
|
|
40
41
|
* @typedef {import('./data.js').Meta} Meta
|
|
41
42
|
* @typedef {import('./data.js').ListParams} ListParams
|
|
42
43
|
* @typedef {import('./data.js').WriteOptions} WriteOptions
|
|
44
|
+
* @typedef {import('./data.js').CreateOptions} CreateOptions
|
|
43
45
|
* @typedef {import('./admin.js').AdminUser} AdminUser
|
|
44
46
|
* @typedef {import('./admin.js').UserPage} UserPage
|
|
45
47
|
* @typedef {import('./admin.js').Invitation} Invitation
|
package/types/storage.d.ts
CHANGED
|
@@ -10,6 +10,12 @@ export type TokenStorage = {
|
|
|
10
10
|
set: (token: string) => void | Promise<void>;
|
|
11
11
|
remove: () => void | Promise<void>;
|
|
12
12
|
};
|
|
13
|
+
/**
|
|
14
|
+
* What the client stores, instead of a token, when the session lives in an
|
|
15
|
+
* HttpOnly cookie (`cookies: true`): a hint that there is a session, with
|
|
16
|
+
* nothing secret in it.
|
|
17
|
+
*/
|
|
18
|
+
export declare const COOKIE_SESSION = "cookie-session";
|
|
13
19
|
/**
|
|
14
20
|
* Keeps the token in memory: it is lost on reload, and scripts on the page
|
|
15
21
|
* can't read it from storage. The default.
|