@fidscript/instant-admin 0.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/dist/commonjs/fidscript-defaults.d.ts +14 -0
- package/dist/commonjs/fidscript-defaults.d.ts.map +1 -0
- package/dist/commonjs/fidscript-defaults.js +17 -0
- package/dist/commonjs/fidscript-defaults.js.map +1 -0
- package/dist/commonjs/index.d.ts +608 -0
- package/dist/commonjs/index.d.ts.map +1 -0
- package/dist/commonjs/index.js +1051 -0
- package/dist/commonjs/index.js.map +1 -0
- package/dist/commonjs/polyfill.d.ts +17 -0
- package/dist/commonjs/polyfill.d.ts.map +1 -0
- package/dist/commonjs/polyfill.js +26 -0
- package/dist/commonjs/polyfill.js.map +1 -0
- package/dist/commonjs/subscribe.d.ts +39 -0
- package/dist/commonjs/subscribe.d.ts.map +1 -0
- package/dist/commonjs/subscribe.js +306 -0
- package/dist/commonjs/subscribe.js.map +1 -0
- package/dist/commonjs/version.d.ts +3 -0
- package/dist/commonjs/version.d.ts.map +1 -0
- package/dist/commonjs/version.js +5 -0
- package/dist/commonjs/version.js.map +1 -0
- package/dist/esm/fidscript-defaults.d.ts +14 -0
- package/dist/esm/fidscript-defaults.d.ts.map +1 -0
- package/dist/esm/fidscript-defaults.js +14 -0
- package/dist/esm/fidscript-defaults.js.map +1 -0
- package/dist/esm/index.d.ts +608 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +1038 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/polyfill.d.ts +17 -0
- package/dist/esm/polyfill.d.ts.map +1 -0
- package/dist/esm/polyfill.js +22 -0
- package/dist/esm/polyfill.js.map +1 -0
- package/dist/esm/subscribe.d.ts +39 -0
- package/dist/esm/subscribe.d.ts.map +1 -0
- package/dist/esm/subscribe.js +300 -0
- package/dist/esm/subscribe.js.map +1 -0
- package/dist/esm/version.d.ts +3 -0
- package/dist/esm/version.d.ts.map +1 -0
- package/dist/esm/version.js +3 -0
- package/dist/esm/version.js.map +1 -0
- package/package.json +33 -0
|
@@ -0,0 +1,1038 @@
|
|
|
1
|
+
import { validate as uuidValidate } from 'uuid';
|
|
2
|
+
import { fidscriptDefaults } from './fidscript-defaults.js';
|
|
3
|
+
import { tx, lookup, getOps, i, id, txInit, version as coreVersion, InstantAPIError, setInstantWarningsEnabled, InstantError, validateQuery, validateTransactions, createInstantRouteHandler, SSEConnection, InstantStream, } from '@fidscript/instant-sdk';
|
|
4
|
+
import version from "./version.js";
|
|
5
|
+
import { subscribe, } from "./subscribe.js";
|
|
6
|
+
import { parseCookie } from 'cookie';
|
|
7
|
+
import { EventSource } from '@instantdb/eventsource';
|
|
8
|
+
import { MessageEventPolyfill } from "./polyfill.js";
|
|
9
|
+
import { Webhooks, WebhooksManager, } from '@instantdb/webhooks';
|
|
10
|
+
function configWithDefaults(config) {
|
|
11
|
+
const defaultConfig = {
|
|
12
|
+
apiURI: fidscriptDefaults.apiURI,
|
|
13
|
+
};
|
|
14
|
+
const r = { ...defaultConfig, ...config };
|
|
15
|
+
return r;
|
|
16
|
+
}
|
|
17
|
+
function instantConfigWithDefaults(config) {
|
|
18
|
+
const defaultConfig = {
|
|
19
|
+
apiURI: fidscriptDefaults.apiURI,
|
|
20
|
+
};
|
|
21
|
+
const r = { ...defaultConfig, ...config };
|
|
22
|
+
if (!r.apiURI) {
|
|
23
|
+
r.apiURI = defaultConfig.apiURI;
|
|
24
|
+
}
|
|
25
|
+
return r;
|
|
26
|
+
}
|
|
27
|
+
function withImpersonation(headers, opts) {
|
|
28
|
+
if ('email' in opts) {
|
|
29
|
+
headers['as-email'] = opts.email;
|
|
30
|
+
}
|
|
31
|
+
else if ('token' in opts) {
|
|
32
|
+
headers['as-token'] = opts.token;
|
|
33
|
+
}
|
|
34
|
+
else if ('guest' in opts) {
|
|
35
|
+
headers['as-guest'] = 'true';
|
|
36
|
+
}
|
|
37
|
+
return headers;
|
|
38
|
+
}
|
|
39
|
+
function validateConfigAndImpersonation(config, impersonationOpts) {
|
|
40
|
+
if (impersonationOpts &&
|
|
41
|
+
('token' in impersonationOpts || 'guest' in impersonationOpts)) {
|
|
42
|
+
// adminToken is not required for `token` or `guest` impersonation
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (config.adminToken) {
|
|
46
|
+
// An adminToken is provided.
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (impersonationOpts && 'email' in impersonationOpts) {
|
|
50
|
+
throw new Error('Admin token required. To impersonate users with an email you must pass `adminToken` to `init`.');
|
|
51
|
+
}
|
|
52
|
+
throw new Error('Admin token required. To run this operation pass `adminToken` to `init`, or use `db.asUser`.');
|
|
53
|
+
}
|
|
54
|
+
function authorizedHeaders(config, impersonationOpts) {
|
|
55
|
+
validateConfigAndImpersonation(config, impersonationOpts);
|
|
56
|
+
const { adminToken, appId } = config;
|
|
57
|
+
const headers = {
|
|
58
|
+
'content-type': 'application/json',
|
|
59
|
+
'app-id': appId,
|
|
60
|
+
};
|
|
61
|
+
if (adminToken) {
|
|
62
|
+
headers.authorization = `Bearer ${adminToken}`;
|
|
63
|
+
}
|
|
64
|
+
return impersonationOpts
|
|
65
|
+
? withImpersonation(headers, impersonationOpts)
|
|
66
|
+
: headers;
|
|
67
|
+
}
|
|
68
|
+
// NextJS 13 and 14 cache fetch requests by default.
|
|
69
|
+
//
|
|
70
|
+
// Since adminDB.query uses fetch, this means that it would also cache by default.
|
|
71
|
+
//
|
|
72
|
+
// We don't want this behavior. `adminDB.query` should return the latest result by default.
|
|
73
|
+
//
|
|
74
|
+
// To get around this, we set an explicit `cache` header for NextJS 13 and 14.
|
|
75
|
+
// This is no longer needed in NextJS 15 onwards, as the default is `no-store` again.
|
|
76
|
+
// Once NextJS 13 and 14 are no longer common, we can remove this code.
|
|
77
|
+
function isNextJSVersionThatCachesFetchByDefault() {
|
|
78
|
+
return (
|
|
79
|
+
// NextJS 13 onwards added a `__nextPatched` property to the fetch function
|
|
80
|
+
fetch['__nextPatched'] &&
|
|
81
|
+
// NextJS 15 onwards _also_ added a global `next-patch` symbol.
|
|
82
|
+
!globalThis[Symbol.for('next-patch')]);
|
|
83
|
+
}
|
|
84
|
+
function getDefaultFetchOpts() {
|
|
85
|
+
return isNextJSVersionThatCachesFetchByDefault() ? { cache: 'no-store' } : {};
|
|
86
|
+
}
|
|
87
|
+
async function jsonReject(rejectFn, res) {
|
|
88
|
+
const body = await res.text();
|
|
89
|
+
try {
|
|
90
|
+
const json = JSON.parse(body);
|
|
91
|
+
return rejectFn(new InstantAPIError({ status: res.status, body: json }));
|
|
92
|
+
}
|
|
93
|
+
catch (_e) {
|
|
94
|
+
return rejectFn(new InstantAPIError({
|
|
95
|
+
status: res.status,
|
|
96
|
+
body: { type: undefined, message: body },
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function jsonFetch(input, init) {
|
|
101
|
+
const defaultFetchOpts = getDefaultFetchOpts();
|
|
102
|
+
const headers = {
|
|
103
|
+
...(init?.headers || {}),
|
|
104
|
+
'Instant-Admin-Version': version,
|
|
105
|
+
'Instant-Core-Version': coreVersion,
|
|
106
|
+
};
|
|
107
|
+
const res = await fetch(input, { ...defaultFetchOpts, ...init, headers });
|
|
108
|
+
if (res.status === 200) {
|
|
109
|
+
const json = await res.json();
|
|
110
|
+
return Promise.resolve(json);
|
|
111
|
+
}
|
|
112
|
+
return jsonReject((x) => Promise.reject(x), res);
|
|
113
|
+
}
|
|
114
|
+
function makeEventSourceWrapper(opts) {
|
|
115
|
+
return class EventSourceWrapper {
|
|
116
|
+
source;
|
|
117
|
+
static OPEN = EventSource.OPEN;
|
|
118
|
+
static CONNECTING = EventSource.CONNECTING;
|
|
119
|
+
static CLOSED = EventSource.CLOSED;
|
|
120
|
+
url;
|
|
121
|
+
constructor(url) {
|
|
122
|
+
this.url = url;
|
|
123
|
+
this.source = this.#createEventSource(url);
|
|
124
|
+
}
|
|
125
|
+
get onopen() {
|
|
126
|
+
return this.source.onopen;
|
|
127
|
+
}
|
|
128
|
+
set onopen(fn) {
|
|
129
|
+
this.source.onopen = fn;
|
|
130
|
+
}
|
|
131
|
+
get onmessage() {
|
|
132
|
+
return this.source.onmessage;
|
|
133
|
+
}
|
|
134
|
+
set onmessage(fn) {
|
|
135
|
+
this.source.onmessage = fn;
|
|
136
|
+
}
|
|
137
|
+
get onerror() {
|
|
138
|
+
return this.source.onerror;
|
|
139
|
+
}
|
|
140
|
+
set onerror(fn) {
|
|
141
|
+
this.source.onerror = fn;
|
|
142
|
+
}
|
|
143
|
+
get readyState() {
|
|
144
|
+
return this.source.readyState;
|
|
145
|
+
}
|
|
146
|
+
close() {
|
|
147
|
+
this.source.close();
|
|
148
|
+
}
|
|
149
|
+
#createEventSource(url) {
|
|
150
|
+
const es = new EventSource(url, {
|
|
151
|
+
messageEvent: MessageEventPolyfill,
|
|
152
|
+
fetch(input, init) {
|
|
153
|
+
return fetch(input, {
|
|
154
|
+
...init,
|
|
155
|
+
method: 'POST',
|
|
156
|
+
headers: opts.headers,
|
|
157
|
+
body: JSON.stringify({
|
|
158
|
+
'inference?': opts.inference,
|
|
159
|
+
versions: {
|
|
160
|
+
'@instantdb/admin': version,
|
|
161
|
+
'@instantdb/core': coreVersion,
|
|
162
|
+
},
|
|
163
|
+
}),
|
|
164
|
+
});
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
return es;
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
*
|
|
173
|
+
* The first step: init your application!
|
|
174
|
+
*
|
|
175
|
+
* Visit https://instantdb.com/dash to get your `appId` :)
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* import { init } from "@instantdb/admin"
|
|
179
|
+
*
|
|
180
|
+
* const db = init({
|
|
181
|
+
* appId: process.env.INSTANT_APP_ID!,
|
|
182
|
+
* adminToken: process.env.INSTANT_APP_ADMIN_TOKEN
|
|
183
|
+
* })
|
|
184
|
+
*
|
|
185
|
+
* // You can also provide a schema for type safety and editor autocomplete!
|
|
186
|
+
*
|
|
187
|
+
* import { init } from "@instantdb/admin"
|
|
188
|
+
* import schema from ""../instant.schema.ts";
|
|
189
|
+
*
|
|
190
|
+
* const db = init({
|
|
191
|
+
* appId: process.env.INSTANT_APP_ID!,
|
|
192
|
+
* adminToken: process.env.INSTANT_APP_ADMIN_TOKEN,
|
|
193
|
+
* schema,
|
|
194
|
+
* })
|
|
195
|
+
* // To learn more: https://instantdb.com/docs/modeling-data
|
|
196
|
+
*/
|
|
197
|
+
function init(
|
|
198
|
+
// Allows config with missing `useDateObjects`, but keeps `UseDates`
|
|
199
|
+
// as a non-nullable in the InstantConfig type.
|
|
200
|
+
config) {
|
|
201
|
+
if (!config.appId || !uuidValidate(config.appId)) {
|
|
202
|
+
console.warn('warning: Instant Admin DB must be initialized with a valid appId. Received: ' +
|
|
203
|
+
JSON.stringify(config.appId));
|
|
204
|
+
}
|
|
205
|
+
const configStrict = {
|
|
206
|
+
...config,
|
|
207
|
+
appId: config.appId?.trim(),
|
|
208
|
+
adminToken: config.adminToken?.trim(),
|
|
209
|
+
useDateObjects: (config.useDateObjects ?? false),
|
|
210
|
+
};
|
|
211
|
+
return new InstantAdminDatabase(configStrict);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* @deprecated
|
|
215
|
+
* `init_experimental` is deprecated. You can replace it with `init`.
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
*
|
|
219
|
+
* // Before
|
|
220
|
+
* import { init_experimental } from "@instantdb/admin"
|
|
221
|
+
* const db = init_experimental({ ... });
|
|
222
|
+
*
|
|
223
|
+
* // After
|
|
224
|
+
* import { init } from "@instantdb/admin"
|
|
225
|
+
* const db = init({ ... });
|
|
226
|
+
*/
|
|
227
|
+
const init_experimental = init;
|
|
228
|
+
function steps(inputChunks) {
|
|
229
|
+
const chunks = Array.isArray(inputChunks) ? inputChunks : [inputChunks];
|
|
230
|
+
return chunks.flatMap(getOps);
|
|
231
|
+
}
|
|
232
|
+
class Rooms {
|
|
233
|
+
config;
|
|
234
|
+
constructor(config) {
|
|
235
|
+
this.config = config;
|
|
236
|
+
}
|
|
237
|
+
async getPresence(roomType, roomId) {
|
|
238
|
+
const res = await jsonFetch(`${this.config.apiURI}/admin/rooms/presence?app_id=${this.config.appId}&room-type=${String(roomType)}&room-id=${roomId}`, {
|
|
239
|
+
method: 'GET',
|
|
240
|
+
headers: authorizedHeaders(this.config),
|
|
241
|
+
});
|
|
242
|
+
return res.sessions || {};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
class Auth {
|
|
246
|
+
config;
|
|
247
|
+
constructor(config) {
|
|
248
|
+
this.config = config;
|
|
249
|
+
this.createToken = this.createToken.bind(this);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Generates a magic code for the user with the given email.
|
|
253
|
+
* This is useful if you want to use your own email provider
|
|
254
|
+
* to send magic codes.
|
|
255
|
+
*
|
|
256
|
+
* @example
|
|
257
|
+
* // Generate a magic code
|
|
258
|
+
* const { code } = await db.auth.generateMagicCode({ email })
|
|
259
|
+
* // Send the magic code to the user with your own email provider
|
|
260
|
+
* await customEmailProvider.sendMagicCode(email, code)
|
|
261
|
+
*
|
|
262
|
+
* @see https://instantdb.com/docs/backend#custom-magic-codes
|
|
263
|
+
*/
|
|
264
|
+
generateMagicCode = async (email) => {
|
|
265
|
+
return jsonFetch(`${this.config.apiURI}/admin/magic_code?app_id=${this.config.appId}`, {
|
|
266
|
+
method: 'POST',
|
|
267
|
+
headers: authorizedHeaders(this.config),
|
|
268
|
+
body: JSON.stringify({ email }),
|
|
269
|
+
});
|
|
270
|
+
};
|
|
271
|
+
/**
|
|
272
|
+
* Sends a magic code to the user with the given email.
|
|
273
|
+
* This uses Instant's built-in email provider.
|
|
274
|
+
*
|
|
275
|
+
* @example
|
|
276
|
+
* // Send an email to user with magic code
|
|
277
|
+
* await db.auth.sendMagicCode({ email })
|
|
278
|
+
*
|
|
279
|
+
* @see https://instantdb.com/docs/backend#custom-magic-codes
|
|
280
|
+
*/
|
|
281
|
+
sendMagicCode = async (email) => {
|
|
282
|
+
return jsonFetch(`${this.config.apiURI}/admin/send_magic_code?app_id=${this.config.appId}`, {
|
|
283
|
+
method: 'POST',
|
|
284
|
+
headers: authorizedHeaders(this.config),
|
|
285
|
+
body: JSON.stringify({ email }),
|
|
286
|
+
});
|
|
287
|
+
};
|
|
288
|
+
/**
|
|
289
|
+
* @deprecated Use {@link checkMagicCode} instead to get the `created` field
|
|
290
|
+
* and support `extraFields`.
|
|
291
|
+
*
|
|
292
|
+
* @see https://instantdb.com/docs/backend#custom-magic-codes
|
|
293
|
+
*/
|
|
294
|
+
verifyMagicCode = async (email, code) => {
|
|
295
|
+
const { user } = await jsonFetch(`${this.config.apiURI}/admin/verify_magic_code?app_id=${this.config.appId}`, {
|
|
296
|
+
method: 'POST',
|
|
297
|
+
headers: authorizedHeaders(this.config),
|
|
298
|
+
body: JSON.stringify({ email, code }),
|
|
299
|
+
});
|
|
300
|
+
return user;
|
|
301
|
+
};
|
|
302
|
+
/**
|
|
303
|
+
* Verifies a magic code and returns the user along with whether
|
|
304
|
+
* the user was newly created. Supports `extraFields` to set custom
|
|
305
|
+
* `$users` properties at signup.
|
|
306
|
+
*
|
|
307
|
+
* @example
|
|
308
|
+
* const { user, created } = await db.auth.checkMagicCode(
|
|
309
|
+
* email,
|
|
310
|
+
* code,
|
|
311
|
+
* { extraFields: { nickname: 'ari' } },
|
|
312
|
+
* );
|
|
313
|
+
*
|
|
314
|
+
* @see https://instantdb.com/docs/backend#custom-magic-codes
|
|
315
|
+
*/
|
|
316
|
+
checkMagicCode = async (email, code, options) => {
|
|
317
|
+
const res = await jsonFetch(`${this.config.apiURI}/admin/verify_magic_code?app_id=${this.config.appId}`, {
|
|
318
|
+
method: 'POST',
|
|
319
|
+
headers: authorizedHeaders(this.config),
|
|
320
|
+
body: JSON.stringify({
|
|
321
|
+
email,
|
|
322
|
+
code,
|
|
323
|
+
...(options?.extraFields
|
|
324
|
+
? { 'extra-fields': options.extraFields }
|
|
325
|
+
: {}),
|
|
326
|
+
}),
|
|
327
|
+
});
|
|
328
|
+
return { user: res.user, created: res.created };
|
|
329
|
+
};
|
|
330
|
+
async createToken(input) {
|
|
331
|
+
const body = typeof input === 'string' ? { email: input } : input;
|
|
332
|
+
const ret = await jsonFetch(`${this.config.apiURI}/admin/refresh_tokens?app_id=${this.config.appId}`, {
|
|
333
|
+
method: 'POST',
|
|
334
|
+
headers: authorizedHeaders(this.config),
|
|
335
|
+
body: JSON.stringify(body),
|
|
336
|
+
});
|
|
337
|
+
return ret.user.refresh_token;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Verifies a given token and returns the associated user.
|
|
341
|
+
*
|
|
342
|
+
* This is often useful for writing custom endpoints, where you need
|
|
343
|
+
* to authenticate users.
|
|
344
|
+
*
|
|
345
|
+
* @example
|
|
346
|
+
* app.post('/custom_endpoint', async (req, res) => {
|
|
347
|
+
* const user = await db.auth.verifyToken(req.headers['token'])
|
|
348
|
+
* if (!user) {
|
|
349
|
+
* return res.status(401).send('Uh oh, you are not authenticated')
|
|
350
|
+
* }
|
|
351
|
+
* // ...
|
|
352
|
+
* })
|
|
353
|
+
* @see https://instantdb.com/docs/backend#custom-endpoints
|
|
354
|
+
*/
|
|
355
|
+
verifyToken = async (token) => {
|
|
356
|
+
const res = await jsonFetch(`${this.config.apiURI}/runtime/auth/verify_refresh_token?app_id=${this.config.appId}`, {
|
|
357
|
+
method: 'POST',
|
|
358
|
+
headers: { 'content-type': 'application/json' },
|
|
359
|
+
body: JSON.stringify({
|
|
360
|
+
'app-id': this.config.appId,
|
|
361
|
+
'refresh-token': token,
|
|
362
|
+
}),
|
|
363
|
+
});
|
|
364
|
+
return res.user;
|
|
365
|
+
};
|
|
366
|
+
/**
|
|
367
|
+
* Retrieves an app user by id, email, or refresh token.
|
|
368
|
+
* Resolves to `null` when no user matches; throws on malformed
|
|
369
|
+
* input or auth errors.
|
|
370
|
+
*
|
|
371
|
+
* @example
|
|
372
|
+
* const user = await db.auth.getUser({ email });
|
|
373
|
+
* if (!user) {
|
|
374
|
+
* console.log("No user found");
|
|
375
|
+
* return;
|
|
376
|
+
* }
|
|
377
|
+
* console.log("Found user:", user);
|
|
378
|
+
*
|
|
379
|
+
* @see https://instantdb.com/docs/backend#retrieve-a-user
|
|
380
|
+
*/
|
|
381
|
+
getUser = async (params) => {
|
|
382
|
+
const qs = new URLSearchParams(Object.entries(params)).toString();
|
|
383
|
+
const response = await jsonFetch(`${this.config.apiURI}/admin/users?app_id=${this.config.appId}&${qs}`, {
|
|
384
|
+
method: 'GET',
|
|
385
|
+
headers: authorizedHeaders(this.config),
|
|
386
|
+
});
|
|
387
|
+
return response.user;
|
|
388
|
+
};
|
|
389
|
+
/**
|
|
390
|
+
* Deletes an app user by id, email, or refresh token.
|
|
391
|
+
* Resolves to `null` when no user matches; throws on malformed
|
|
392
|
+
* input or auth errors.
|
|
393
|
+
*
|
|
394
|
+
* NB: This _only_ deletes the user; it does not delete all user data.
|
|
395
|
+
* You will need to handle this manually.
|
|
396
|
+
*
|
|
397
|
+
* @example
|
|
398
|
+
* const deletedUser = await db.auth.deleteUser({ email });
|
|
399
|
+
* if (!deletedUser) {
|
|
400
|
+
* console.log("No user found to delete");
|
|
401
|
+
* return;
|
|
402
|
+
* }
|
|
403
|
+
* console.log("Deleted user:", deletedUser);
|
|
404
|
+
*
|
|
405
|
+
* @see https://instantdb.com/docs/backend#delete-a-user
|
|
406
|
+
*/
|
|
407
|
+
deleteUser = async (params) => {
|
|
408
|
+
const qs = new URLSearchParams(Object.entries(params)).toString();
|
|
409
|
+
const response = await jsonFetch(`${this.config.apiURI}/admin/users?app_id=${this.config.appId}&${qs}`, {
|
|
410
|
+
method: 'DELETE',
|
|
411
|
+
headers: authorizedHeaders(this.config),
|
|
412
|
+
});
|
|
413
|
+
return response.deleted;
|
|
414
|
+
};
|
|
415
|
+
async signOut(input) {
|
|
416
|
+
// If input is a string, we assume it's an email.
|
|
417
|
+
// This is because of backwards compatibility: we used to only
|
|
418
|
+
// accept email strings. Eventually we can remove this
|
|
419
|
+
const params = typeof input === 'string' ? { email: input } : input;
|
|
420
|
+
const config = this.config;
|
|
421
|
+
await jsonFetch(`${config.apiURI}/admin/sign_out?app_id=${this.config.appId}`, {
|
|
422
|
+
method: 'POST',
|
|
423
|
+
headers: authorizedHeaders(config),
|
|
424
|
+
body: JSON.stringify(params),
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Get instant user from Request
|
|
429
|
+
*
|
|
430
|
+
* Reads cookies and gets a validated user
|
|
431
|
+
* @param req The request containing a cookie synced with createInstantRouteHandler
|
|
432
|
+
* @param opts Allow disabling validation of refresh token
|
|
433
|
+
*/
|
|
434
|
+
getUserFromRequest = async (req, opts) => {
|
|
435
|
+
const cookieHeader = req.headers.get('cookie') || '';
|
|
436
|
+
const parsedCookie = parseCookie(cookieHeader);
|
|
437
|
+
const cookieName = 'instant_user_' + this.config.appId;
|
|
438
|
+
if (!parsedCookie[cookieName]) {
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
const value = parsedCookie[cookieName];
|
|
442
|
+
const user = JSON.parse(value);
|
|
443
|
+
if (!user?.refresh_token) {
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
if (opts?.disableValidation) {
|
|
447
|
+
return user;
|
|
448
|
+
}
|
|
449
|
+
const verified = await this.verifyToken(user.refresh_token);
|
|
450
|
+
return verified;
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
const isNodeReadable = (v) => v &&
|
|
454
|
+
typeof v === 'object' &&
|
|
455
|
+
typeof v.pipe === 'function' &&
|
|
456
|
+
typeof v.read === 'function';
|
|
457
|
+
const isWebReadable = (v) => v && typeof v.getReader === 'function';
|
|
458
|
+
/**
|
|
459
|
+
* Functions to manage file storage.
|
|
460
|
+
*/
|
|
461
|
+
class Storage {
|
|
462
|
+
config;
|
|
463
|
+
impersonationOpts;
|
|
464
|
+
constructor(config, impersonationOpts) {
|
|
465
|
+
this.config = config;
|
|
466
|
+
this.impersonationOpts = impersonationOpts;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Uploads file at the provided path. Accepts a Buffer or a Readable stream.
|
|
470
|
+
*
|
|
471
|
+
* @see https://instantdb.com/docs/storage
|
|
472
|
+
* @example
|
|
473
|
+
* const buffer = fs.readFileSync('demo.png');
|
|
474
|
+
* const isSuccess = await db.storage.uploadFile('photos/demo.png', buffer);
|
|
475
|
+
*/
|
|
476
|
+
uploadFile = async (path, file, metadata = {}) => {
|
|
477
|
+
const headers = {
|
|
478
|
+
...authorizedHeaders(this.config, this.impersonationOpts),
|
|
479
|
+
path,
|
|
480
|
+
};
|
|
481
|
+
if (metadata.contentDisposition) {
|
|
482
|
+
headers['content-disposition'] = metadata.contentDisposition;
|
|
483
|
+
}
|
|
484
|
+
// headers.content-type will become "undefined" (string)
|
|
485
|
+
// if not removed from the object
|
|
486
|
+
delete headers['content-type'];
|
|
487
|
+
if (metadata.contentType) {
|
|
488
|
+
headers['content-type'] = metadata.contentType;
|
|
489
|
+
}
|
|
490
|
+
let duplex;
|
|
491
|
+
if (isNodeReadable(file)) {
|
|
492
|
+
duplex = 'half'; // one-way stream
|
|
493
|
+
}
|
|
494
|
+
if (isNodeReadable(file) || isWebReadable(file)) {
|
|
495
|
+
if (!metadata.fileSize) {
|
|
496
|
+
throw new Error('fileSize is required in metadata when uploading streams');
|
|
497
|
+
}
|
|
498
|
+
headers['content-length'] = metadata.fileSize.toString();
|
|
499
|
+
}
|
|
500
|
+
let options = {
|
|
501
|
+
method: 'PUT',
|
|
502
|
+
headers,
|
|
503
|
+
body: file,
|
|
504
|
+
...(duplex && { duplex }),
|
|
505
|
+
};
|
|
506
|
+
return jsonFetch(`${this.config.apiURI}/admin/storage/upload?app_id=${this.config.appId}`, options);
|
|
507
|
+
};
|
|
508
|
+
/**
|
|
509
|
+
* Deletes a file by its path name (e.g. "photos/demo.png").
|
|
510
|
+
*
|
|
511
|
+
* @deprecated Use `db.transact` to delete files instead:
|
|
512
|
+
* @example
|
|
513
|
+
* // Delete by id
|
|
514
|
+
* await db.transact(db.tx.$files[fileId].delete());
|
|
515
|
+
*
|
|
516
|
+
* // Delete by path
|
|
517
|
+
* await db.transact(db.tx.$files[lookup('path', 'photos/demo.png')].delete());
|
|
518
|
+
*
|
|
519
|
+
* @see https://instantdb.com/docs/storage
|
|
520
|
+
*/
|
|
521
|
+
delete = async (pathname) => {
|
|
522
|
+
return jsonFetch(`${this.config.apiURI}/admin/storage/files?app_id=${this.config.appId}&filename=${encodeURIComponent(pathname)}`, {
|
|
523
|
+
method: 'DELETE',
|
|
524
|
+
headers: authorizedHeaders(this.config, this.impersonationOpts),
|
|
525
|
+
});
|
|
526
|
+
};
|
|
527
|
+
/**
|
|
528
|
+
* Deletes multiple files by their path names.
|
|
529
|
+
*
|
|
530
|
+
* @deprecated Use `db.transact` to delete files instead:
|
|
531
|
+
* @example
|
|
532
|
+
* // Delete multiple files by path
|
|
533
|
+
* const paths = ['images/1.png', 'images/2.png', 'images/3.png'];
|
|
534
|
+
* await db.transact(paths.map(p => db.tx.$files[lookup('path', p)].delete()));
|
|
535
|
+
*
|
|
536
|
+
* @see https://instantdb.com/docs/storage
|
|
537
|
+
*/
|
|
538
|
+
deleteMany = async (pathnames) => {
|
|
539
|
+
return jsonFetch(`${this.config.apiURI}/admin/storage/files/delete?app_id=${this.config.appId}`, {
|
|
540
|
+
method: 'POST',
|
|
541
|
+
headers: authorizedHeaders(this.config, this.impersonationOpts),
|
|
542
|
+
body: JSON.stringify({ filenames: pathnames }),
|
|
543
|
+
});
|
|
544
|
+
};
|
|
545
|
+
/**
|
|
546
|
+
* @deprecated. This method will be removed in the future. Use `uploadFile`
|
|
547
|
+
* instead
|
|
548
|
+
*/
|
|
549
|
+
upload = async (pathname, file, metadata = {}) => {
|
|
550
|
+
const { data: presignedUrl } = await jsonFetch(`${this.config.apiURI}/admin/storage/signed-upload-url?app_id=${this.config.appId}`, {
|
|
551
|
+
method: 'POST',
|
|
552
|
+
headers: authorizedHeaders(this.config),
|
|
553
|
+
body: JSON.stringify({
|
|
554
|
+
app_id: this.config.appId,
|
|
555
|
+
filename: pathname,
|
|
556
|
+
}),
|
|
557
|
+
});
|
|
558
|
+
const headers = {};
|
|
559
|
+
const contentType = metadata.contentType;
|
|
560
|
+
if (contentType) {
|
|
561
|
+
headers['Content-Type'] = contentType;
|
|
562
|
+
}
|
|
563
|
+
const { ok } = await fetch(presignedUrl, {
|
|
564
|
+
method: 'PUT',
|
|
565
|
+
body: file,
|
|
566
|
+
headers,
|
|
567
|
+
});
|
|
568
|
+
return ok;
|
|
569
|
+
};
|
|
570
|
+
/**
|
|
571
|
+
* @deprecated. This method will be removed in the future. Use `query` instead
|
|
572
|
+
* @example
|
|
573
|
+
* const files = await db.query({ $files: {}})
|
|
574
|
+
*/
|
|
575
|
+
list = async () => {
|
|
576
|
+
const { data } = await jsonFetch(`${this.config.apiURI}/admin/storage/files?app_id=${this.config.appId}`, {
|
|
577
|
+
method: 'GET',
|
|
578
|
+
headers: authorizedHeaders(this.config),
|
|
579
|
+
});
|
|
580
|
+
return data;
|
|
581
|
+
};
|
|
582
|
+
/**
|
|
583
|
+
* @deprecated. getDownloadUrl will be removed in the future.
|
|
584
|
+
* Use `query` instead to query and fetch for valid urls
|
|
585
|
+
*
|
|
586
|
+
* db.useQuery({
|
|
587
|
+
* $files: {
|
|
588
|
+
* $: {
|
|
589
|
+
* where: {
|
|
590
|
+
* path: "moop.png"
|
|
591
|
+
* }
|
|
592
|
+
* }
|
|
593
|
+
* }
|
|
594
|
+
* })
|
|
595
|
+
*/
|
|
596
|
+
getDownloadUrl = async (pathname) => {
|
|
597
|
+
const { data } = await jsonFetch(`${this.config.apiURI}/admin/storage/signed-download-url?app_id=${this.config.appId}&filename=${encodeURIComponent(pathname)}`, {
|
|
598
|
+
method: 'GET',
|
|
599
|
+
headers: authorizedHeaders(this.config),
|
|
600
|
+
});
|
|
601
|
+
return data;
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Functions to manage streams.
|
|
606
|
+
*/
|
|
607
|
+
class Streams {
|
|
608
|
+
#ensureInstantStream;
|
|
609
|
+
constructor(ensureInstantStream) {
|
|
610
|
+
this.#ensureInstantStream = ensureInstantStream;
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Creates a new ReadableStream for the given clientId.
|
|
614
|
+
*
|
|
615
|
+
* @example
|
|
616
|
+
* const stream = db.streams.createReadStream({clientId: clientId})
|
|
617
|
+
* for await (const chunk of stream) {
|
|
618
|
+
* console.log(chunk);
|
|
619
|
+
* }
|
|
620
|
+
*/
|
|
621
|
+
createReadStream = (opts) => {
|
|
622
|
+
return this.#ensureInstantStream().createReadStream(opts);
|
|
623
|
+
};
|
|
624
|
+
/**
|
|
625
|
+
* Creates a new WritableStream for the given clientId.
|
|
626
|
+
*
|
|
627
|
+
* @example
|
|
628
|
+
* const writeStream = db.streams.createWriteStream({clientId: clientId})
|
|
629
|
+
* const writer = writeStream.getWriter();
|
|
630
|
+
* writer.write('Hello world');
|
|
631
|
+
* writer.close();
|
|
632
|
+
*/
|
|
633
|
+
createWriteStream = (opts) => {
|
|
634
|
+
return this.#ensureInstantStream().createWriteStream(opts);
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
function createLogger(isEnabled, baseLogger = console) {
|
|
638
|
+
return {
|
|
639
|
+
info: isEnabled ? (...args) => baseLogger.info(...args) : () => { },
|
|
640
|
+
debug: isEnabled ? (...args) => baseLogger.debug(...args) : () => { },
|
|
641
|
+
error: isEnabled ? (...args) => baseLogger.error(...args) : () => { },
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
*
|
|
646
|
+
* The first step: init your application!
|
|
647
|
+
*
|
|
648
|
+
* Visit https://instantdb.com/dash to get your `appId` and `adminToken` :)
|
|
649
|
+
*
|
|
650
|
+
* @example
|
|
651
|
+
* const db = init({ appId: "my-app-id", adminToken: "my-admin-token" })
|
|
652
|
+
*/
|
|
653
|
+
class InstantAdminDatabase {
|
|
654
|
+
config;
|
|
655
|
+
auth;
|
|
656
|
+
storage;
|
|
657
|
+
streams;
|
|
658
|
+
rooms;
|
|
659
|
+
impersonationOpts;
|
|
660
|
+
webhooks;
|
|
661
|
+
#sseConnection = null;
|
|
662
|
+
#sseBackoff = 0;
|
|
663
|
+
#instantStream = null;
|
|
664
|
+
#log;
|
|
665
|
+
tx = txInit();
|
|
666
|
+
constructor(_config) {
|
|
667
|
+
this.config = instantConfigWithDefaults(_config);
|
|
668
|
+
this.auth = new Auth(this.config);
|
|
669
|
+
this.storage = new Storage(this.config, this.impersonationOpts);
|
|
670
|
+
this.streams = new Streams(this.#ensureInstantStream.bind(this));
|
|
671
|
+
this.rooms = new Rooms(this.config);
|
|
672
|
+
this.webhooks = new Webhooks(this.config, jsonFetch);
|
|
673
|
+
this.#log = createLogger(!!this.config.verbose, this.config.logger);
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Sometimes you want to scope queries to a specific user.
|
|
677
|
+
*
|
|
678
|
+
* You can provide a user's auth token, email, or impersonate a guest.
|
|
679
|
+
*
|
|
680
|
+
* @see https://instantdb.com/docs/backend#impersonating-users
|
|
681
|
+
* @example
|
|
682
|
+
* await db.asUser({email: "stopa@instantdb.com"}).query({ goals: {} })
|
|
683
|
+
*/
|
|
684
|
+
asUser = (opts) => {
|
|
685
|
+
const newClient = new InstantAdminDatabase({
|
|
686
|
+
...this.config,
|
|
687
|
+
});
|
|
688
|
+
newClient.impersonationOpts = opts;
|
|
689
|
+
newClient.storage = new Storage(this.config, opts);
|
|
690
|
+
return newClient;
|
|
691
|
+
};
|
|
692
|
+
/**
|
|
693
|
+
* Use this to query your data!
|
|
694
|
+
*
|
|
695
|
+
* @see https://instantdb.com/docs/instaql
|
|
696
|
+
*
|
|
697
|
+
* @example
|
|
698
|
+
* // fetch all goals
|
|
699
|
+
* await db.query({ goals: {} })
|
|
700
|
+
*
|
|
701
|
+
* // goals where the title is "Get Fit"
|
|
702
|
+
* await db.query({ goals: { $: { where: { title: "Get Fit" } } } })
|
|
703
|
+
*
|
|
704
|
+
* // all goals, _alongside_ their todos
|
|
705
|
+
* await db.query({ goals: { todos: {} } })
|
|
706
|
+
*/
|
|
707
|
+
query = (query, opts = {}) => {
|
|
708
|
+
if (query && opts && 'ruleParams' in opts) {
|
|
709
|
+
query = { $$ruleParams: opts['ruleParams'], ...query };
|
|
710
|
+
}
|
|
711
|
+
if (!this.config.disableValidation) {
|
|
712
|
+
validateQuery(query, this.config.schema);
|
|
713
|
+
}
|
|
714
|
+
const fetchOpts = opts.fetchOpts || {};
|
|
715
|
+
const fetchOptsHeaders = fetchOpts['headers'] || {};
|
|
716
|
+
return jsonFetch(`${this.config.apiURI}/admin/query?app_id=${this.config.appId}`, {
|
|
717
|
+
...fetchOpts,
|
|
718
|
+
method: 'POST',
|
|
719
|
+
headers: {
|
|
720
|
+
...fetchOptsHeaders,
|
|
721
|
+
...authorizedHeaders(this.config, this.impersonationOpts),
|
|
722
|
+
},
|
|
723
|
+
body: JSON.stringify({
|
|
724
|
+
query: query,
|
|
725
|
+
'inference?': !!this.config.schema,
|
|
726
|
+
}),
|
|
727
|
+
});
|
|
728
|
+
};
|
|
729
|
+
/**
|
|
730
|
+
* Use this to to get a live view of your data!
|
|
731
|
+
*
|
|
732
|
+
* @see https://www.instantdb.com/docs/backend
|
|
733
|
+
*
|
|
734
|
+
* @example
|
|
735
|
+
* // create a subscription to a query
|
|
736
|
+
* const query = { goals: { $: { where: { title: "Get Fit" } } } }
|
|
737
|
+
* const sub = db.subscribeQuery(query);
|
|
738
|
+
*
|
|
739
|
+
* // iterate through the results with an async iterator
|
|
740
|
+
* for await (const payload of sub) {
|
|
741
|
+
* if (payload.error) {
|
|
742
|
+
* console.log(payload.error);
|
|
743
|
+
* // Stop the subscription
|
|
744
|
+
* sub.close();
|
|
745
|
+
* } else {
|
|
746
|
+
* console.log(payload.data);
|
|
747
|
+
* }
|
|
748
|
+
* }
|
|
749
|
+
*
|
|
750
|
+
* // Stop the subscription
|
|
751
|
+
* sub.close();
|
|
752
|
+
*
|
|
753
|
+
* // Create a subscription with a callback
|
|
754
|
+
* const sub = db.subscribeQuery(query, (payload) => {
|
|
755
|
+
* if (payload.error) {
|
|
756
|
+
* console.log(payload.error);
|
|
757
|
+
* // Stop the subscription
|
|
758
|
+
* sub.close();
|
|
759
|
+
* } else {
|
|
760
|
+
* console.log(payload.data);
|
|
761
|
+
* }
|
|
762
|
+
* });
|
|
763
|
+
*/
|
|
764
|
+
subscribeQuery(query, cb, opts = {}) {
|
|
765
|
+
if (query && opts && 'ruleParams' in opts) {
|
|
766
|
+
query = { $$ruleParams: opts['ruleParams'], ...query };
|
|
767
|
+
}
|
|
768
|
+
if (!this.config.disableValidation) {
|
|
769
|
+
validateQuery(query, this.config.schema);
|
|
770
|
+
}
|
|
771
|
+
const fetchOpts = opts.fetchOpts || {};
|
|
772
|
+
const fetchOptsHeaders = fetchOpts['headers'] || {};
|
|
773
|
+
const headers = {
|
|
774
|
+
...fetchOptsHeaders,
|
|
775
|
+
...authorizedHeaders(this.config, this.impersonationOpts),
|
|
776
|
+
};
|
|
777
|
+
const inference = !!this.config.schema;
|
|
778
|
+
return subscribe(query, cb, {
|
|
779
|
+
headers,
|
|
780
|
+
inference,
|
|
781
|
+
apiURI: this.config.apiURI,
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Use this to write data! You can create, update, delete, and link objects
|
|
786
|
+
*
|
|
787
|
+
* @see https://instantdb.com/docs/instaml
|
|
788
|
+
*
|
|
789
|
+
* @example
|
|
790
|
+
* // Create a new object in the `goals` namespace
|
|
791
|
+
* const goalId = id();
|
|
792
|
+
* db.transact(db.tx.goals[goalId].update({title: "Get fit"}))
|
|
793
|
+
*
|
|
794
|
+
* // Update the title
|
|
795
|
+
* db.transact(db.tx.goals[goalId].update({title: "Get super fit"}))
|
|
796
|
+
*
|
|
797
|
+
* // Delete it
|
|
798
|
+
* db.transact(db.tx.goals[goalId].delete())
|
|
799
|
+
*
|
|
800
|
+
* // Or create an association:
|
|
801
|
+
* todoId = id();
|
|
802
|
+
* db.transact([
|
|
803
|
+
* db.tx.todos[todoId].update({ title: 'Go on a run' }),
|
|
804
|
+
* db.tx.goals[goalId].link({todos: todoId}),
|
|
805
|
+
* ])
|
|
806
|
+
*/
|
|
807
|
+
transact = (inputChunks) => {
|
|
808
|
+
if (!this.config.disableValidation) {
|
|
809
|
+
validateTransactions(inputChunks, this.config.schema);
|
|
810
|
+
}
|
|
811
|
+
return jsonFetch(`${this.config.apiURI}/admin/transact?app_id=${this.config.appId}`, {
|
|
812
|
+
method: 'POST',
|
|
813
|
+
headers: authorizedHeaders(this.config, this.impersonationOpts),
|
|
814
|
+
body: JSON.stringify({
|
|
815
|
+
steps: steps(inputChunks),
|
|
816
|
+
'throw-on-missing-attrs?': !!this.config.schema,
|
|
817
|
+
}),
|
|
818
|
+
});
|
|
819
|
+
};
|
|
820
|
+
/**
|
|
821
|
+
* Like `query`, but returns debugging information
|
|
822
|
+
* for permissions checks along with the result.
|
|
823
|
+
* Useful for inspecting the values returned by the permissions checks.
|
|
824
|
+
* Note, this will return debug information for *all* entities
|
|
825
|
+
* that match the query's `where` clauses.
|
|
826
|
+
*
|
|
827
|
+
* Requires a user/guest context to be set with `asUser`,
|
|
828
|
+
* since permissions checks are user-specific.
|
|
829
|
+
*
|
|
830
|
+
* Accepts an optional configuration object with a `rules` key.
|
|
831
|
+
* The provided rules will override the rules in the database for the query.
|
|
832
|
+
*
|
|
833
|
+
* @see https://instantdb.com/docs/instaql
|
|
834
|
+
*
|
|
835
|
+
* @example
|
|
836
|
+
* await db.asUser({ guest: true }).debugQuery(
|
|
837
|
+
* { goals: {} },
|
|
838
|
+
* { rules: { goals: { allow: { read: "auth.id != null" } } }
|
|
839
|
+
* )
|
|
840
|
+
*/
|
|
841
|
+
debugQuery = async (query, opts) => {
|
|
842
|
+
if (query && opts && 'ruleParams' in opts) {
|
|
843
|
+
query = { $$ruleParams: opts['ruleParams'], ...query };
|
|
844
|
+
}
|
|
845
|
+
const body = {
|
|
846
|
+
query,
|
|
847
|
+
'rules-override': opts?.rules,
|
|
848
|
+
'inference?': opts?.cardinalityInference ?? !!this.config.schema,
|
|
849
|
+
};
|
|
850
|
+
if (opts?.ip) {
|
|
851
|
+
body['ip-override'] = opts.ip;
|
|
852
|
+
}
|
|
853
|
+
if (opts?.origin) {
|
|
854
|
+
body['origin-override'] = opts.origin;
|
|
855
|
+
}
|
|
856
|
+
const response = await jsonFetch(`${this.config.apiURI}/admin/query_perms_check?app_id=${this.config.appId}`, {
|
|
857
|
+
method: 'POST',
|
|
858
|
+
headers: authorizedHeaders(this.config, this.impersonationOpts),
|
|
859
|
+
body: JSON.stringify(body),
|
|
860
|
+
});
|
|
861
|
+
return {
|
|
862
|
+
result: response.result,
|
|
863
|
+
checkResults: response['check-results'],
|
|
864
|
+
};
|
|
865
|
+
};
|
|
866
|
+
/**
|
|
867
|
+
* Like `transact`, but does not write to the database.
|
|
868
|
+
* Returns debugging information for permissions checks.
|
|
869
|
+
* Useful for inspecting the values returned by the permissions checks.
|
|
870
|
+
*
|
|
871
|
+
* Requires a user/guest context to be set with `asUser`,
|
|
872
|
+
* since permissions checks are user-specific.
|
|
873
|
+
*
|
|
874
|
+
* Accepts an optional configuration object with a `rules` key.
|
|
875
|
+
* The provided rules will override the rules in the database for the duration of the transaction.
|
|
876
|
+
*
|
|
877
|
+
* @example
|
|
878
|
+
* const goalId = id();
|
|
879
|
+
* db.asUser({ guest: true }).debugTransact(
|
|
880
|
+
* [db.tx.goals[goalId].update({title: "Get fit"})],
|
|
881
|
+
* { rules: { goals: { allow: { update: "auth.id != null" } } }
|
|
882
|
+
* )
|
|
883
|
+
*/
|
|
884
|
+
debugTransact = (inputChunks, opts) => {
|
|
885
|
+
const body = {
|
|
886
|
+
steps: steps(inputChunks),
|
|
887
|
+
'rules-override': opts?.rules,
|
|
888
|
+
// @ts-expect-error because we're using a private API (for now)
|
|
889
|
+
'dangerously-commit-tx': opts?.__dangerouslyCommit,
|
|
890
|
+
};
|
|
891
|
+
if (opts?.ip) {
|
|
892
|
+
body['ip-override'] = opts.ip;
|
|
893
|
+
}
|
|
894
|
+
if (opts?.origin) {
|
|
895
|
+
body['origin-override'] = opts.origin;
|
|
896
|
+
}
|
|
897
|
+
return jsonFetch(`${this.config.apiURI}/admin/transact_perms_check?app_id=${this.config.appId}`, {
|
|
898
|
+
method: 'POST',
|
|
899
|
+
headers: authorizedHeaders(this.config, this.impersonationOpts),
|
|
900
|
+
body: JSON.stringify(body),
|
|
901
|
+
});
|
|
902
|
+
};
|
|
903
|
+
#setupSSEConnection() {
|
|
904
|
+
if (this.#sseConnection) {
|
|
905
|
+
this.#sseConnection.close();
|
|
906
|
+
}
|
|
907
|
+
const headers = {
|
|
908
|
+
...authorizedHeaders(this.config, this.impersonationOpts),
|
|
909
|
+
};
|
|
910
|
+
const inference = !!this.config.schema;
|
|
911
|
+
const ES = makeEventSourceWrapper({ headers, inference });
|
|
912
|
+
const conn = new SSEConnection(ES, `${this.config.apiURI}/admin/sse?app_id=${this.config.appId}`, `${this.config.apiURI}/admin/sse/push?app_id=${this.config.appId}`);
|
|
913
|
+
conn.onopen = this.#onopen;
|
|
914
|
+
conn.onmessage = this.#onmessage;
|
|
915
|
+
conn.onclose = this.#onclose;
|
|
916
|
+
conn.onerror = this.#onerror;
|
|
917
|
+
this.#sseConnection = conn;
|
|
918
|
+
return conn;
|
|
919
|
+
}
|
|
920
|
+
#ensureSSEConnection() {
|
|
921
|
+
return this.#sseConnection || this.#setupSSEConnection();
|
|
922
|
+
}
|
|
923
|
+
#trySend(eventId, msg) {
|
|
924
|
+
const sseConnection = this.#ensureSSEConnection();
|
|
925
|
+
this.#log.info('[send]', eventId, msg, {
|
|
926
|
+
isOpen: sseConnection.isOpen(),
|
|
927
|
+
});
|
|
928
|
+
if (sseConnection.isOpen()) {
|
|
929
|
+
sseConnection.send({ 'client-event-id': eventId, ...msg });
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
#setupInstantStream() {
|
|
933
|
+
this.#ensureSSEConnection();
|
|
934
|
+
const instantStream = new InstantStream({
|
|
935
|
+
WStream: this.config.WritableStream || WritableStream,
|
|
936
|
+
RStream: this.config.ReadableStream || ReadableStream,
|
|
937
|
+
trySend: (eventId, msg) => {
|
|
938
|
+
this.#trySend(eventId, msg);
|
|
939
|
+
},
|
|
940
|
+
log: this.#log,
|
|
941
|
+
});
|
|
942
|
+
this.#instantStream = instantStream;
|
|
943
|
+
return instantStream;
|
|
944
|
+
}
|
|
945
|
+
#ensureInstantStream() {
|
|
946
|
+
return this.#instantStream || this.#setupInstantStream();
|
|
947
|
+
}
|
|
948
|
+
#onopen = (e) => {
|
|
949
|
+
if (e.target !== this.#sseConnection) {
|
|
950
|
+
this.#log.info('[socket][open]', e.target.id, 'skip; this is no longer the current transport');
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
this.#log.info('[socket][open]', e.target.id);
|
|
954
|
+
this.#sseBackoff = 0;
|
|
955
|
+
this.#instantStream?.onConnectionStatusChange('authenticated');
|
|
956
|
+
};
|
|
957
|
+
#onclose = (e) => {
|
|
958
|
+
if (e.target !== this.#sseConnection) {
|
|
959
|
+
this.#log.info('[socket][close]', e.target.id, 'skip; this is no longer the current transport');
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
this.#log.info('[socket][close]', e.target.id);
|
|
963
|
+
this.#instantStream?.onConnectionStatusChange('closed');
|
|
964
|
+
if (this.#sseConnection) {
|
|
965
|
+
this.#sseConnection = null;
|
|
966
|
+
if (!this.#connectionIsIdle()) {
|
|
967
|
+
// We didn't remove the sse connection, and we have streams we care about, so let's try again
|
|
968
|
+
setTimeout(() => this.#ensureSSEConnection(), this.#sseBackoff);
|
|
969
|
+
this.#sseBackoff = Math.min(15000, Math.max(this.#sseBackoff, 500) * 2);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
};
|
|
973
|
+
#onerror = (e) => {
|
|
974
|
+
if (e.target !== this.#sseConnection) {
|
|
975
|
+
this.#log.info('[socket][error]', e.target.id, 'skip; this is no longer the current transport');
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
this.#log.info('[socket][error]', e.target.id);
|
|
979
|
+
this.#instantStream?.onConnectionStatusChange('closed');
|
|
980
|
+
};
|
|
981
|
+
#connectionIsIdle() {
|
|
982
|
+
return !this.#instantStream || !this.#instantStream.hasActiveStreams();
|
|
983
|
+
}
|
|
984
|
+
#maybeShutdownConnection() {
|
|
985
|
+
if (this.#sseConnection && this.#connectionIsIdle()) {
|
|
986
|
+
const conn = this.#sseConnection;
|
|
987
|
+
this.#log.info('cleaning up unused socket', conn.id);
|
|
988
|
+
this.#sseConnection = null;
|
|
989
|
+
conn.close();
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
#onmessage = (e) => {
|
|
993
|
+
if (e.target !== this.#sseConnection) {
|
|
994
|
+
this.#log.info('[socket][message]', e.target.id, 'skip; this is no longer the current transport');
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
const msg = e.message;
|
|
998
|
+
this.#log.info('[receive]', msg);
|
|
999
|
+
switch (msg.op) {
|
|
1000
|
+
case 'start-stream-ok': {
|
|
1001
|
+
this.#instantStream?.onStartStreamOk(msg);
|
|
1002
|
+
break;
|
|
1003
|
+
}
|
|
1004
|
+
case 'stream-flushed': {
|
|
1005
|
+
this.#instantStream?.onStreamFlushed(msg);
|
|
1006
|
+
break;
|
|
1007
|
+
}
|
|
1008
|
+
case 'append-failed': {
|
|
1009
|
+
this.#instantStream?.onAppendFailed(msg);
|
|
1010
|
+
break;
|
|
1011
|
+
}
|
|
1012
|
+
case 'stream-append': {
|
|
1013
|
+
this.#instantStream?.onStreamAppend(msg);
|
|
1014
|
+
break;
|
|
1015
|
+
}
|
|
1016
|
+
case 'error': {
|
|
1017
|
+
switch (msg['original-event']?.op) {
|
|
1018
|
+
case 'start-stream':
|
|
1019
|
+
case 'append-stream':
|
|
1020
|
+
case 'subscribe-stream':
|
|
1021
|
+
case 'unsubscribe-stream': {
|
|
1022
|
+
this.#instantStream?.onRecieveError(msg);
|
|
1023
|
+
break;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
break;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
// Closes the connection if we don't have any items pending
|
|
1030
|
+
this.#maybeShutdownConnection();
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
export { init, init_experimental, id, tx, lookup, i, createInstantRouteHandler, Webhooks, WebhooksManager,
|
|
1034
|
+
// error
|
|
1035
|
+
InstantAPIError,
|
|
1036
|
+
// warnings
|
|
1037
|
+
setInstantWarningsEnabled, InstantError, };
|
|
1038
|
+
//# sourceMappingURL=index.js.map
|