@roko-app/instant-admin 0.0.0-stage → 1.0.67-roko.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/.tshy/build.json +8 -0
  2. package/.tshy/commonjs.json +16 -0
  3. package/.tshy/esm.json +15 -0
  4. package/LICENSE +202 -0
  5. package/README.md +43 -2
  6. package/dist/commonjs/__types__/typeUtils.d.ts +20 -0
  7. package/dist/commonjs/__types__/typeUtils.d.ts.map +1 -0
  8. package/dist/commonjs/__types__/typeUtils.js +5 -0
  9. package/dist/commonjs/__types__/typeUtils.js.map +1 -0
  10. package/dist/commonjs/__types__/typesTests.d.ts +2 -0
  11. package/dist/commonjs/__types__/typesTests.d.ts.map +1 -0
  12. package/dist/commonjs/__types__/typesTests.js +88 -0
  13. package/dist/commonjs/__types__/typesTests.js.map +1 -0
  14. package/dist/commonjs/index.d.ts +608 -0
  15. package/dist/commonjs/index.d.ts.map +1 -0
  16. package/dist/commonjs/index.js +1050 -0
  17. package/dist/commonjs/index.js.map +1 -0
  18. package/dist/commonjs/package.json +3 -0
  19. package/dist/commonjs/polyfill.d.ts +17 -0
  20. package/dist/commonjs/polyfill.d.ts.map +1 -0
  21. package/dist/commonjs/polyfill.js +26 -0
  22. package/dist/commonjs/polyfill.js.map +1 -0
  23. package/dist/commonjs/subscribe.d.ts +39 -0
  24. package/dist/commonjs/subscribe.d.ts.map +1 -0
  25. package/dist/commonjs/subscribe.js +306 -0
  26. package/dist/commonjs/subscribe.js.map +1 -0
  27. package/dist/commonjs/version.d.ts +3 -0
  28. package/dist/commonjs/version.d.ts.map +1 -0
  29. package/dist/commonjs/version.js +5 -0
  30. package/dist/commonjs/version.js.map +1 -0
  31. package/dist/esm/__types__/typeUtils.d.ts +20 -0
  32. package/dist/esm/__types__/typeUtils.d.ts.map +1 -0
  33. package/dist/esm/__types__/typeUtils.js +4 -0
  34. package/dist/esm/__types__/typeUtils.js.map +1 -0
  35. package/dist/esm/__types__/typesTests.d.ts +2 -0
  36. package/dist/esm/__types__/typesTests.d.ts.map +1 -0
  37. package/dist/esm/__types__/typesTests.js +86 -0
  38. package/dist/esm/__types__/typesTests.js.map +1 -0
  39. package/dist/esm/index.d.ts +608 -0
  40. package/dist/esm/index.d.ts.map +1 -0
  41. package/dist/esm/index.js +1037 -0
  42. package/dist/esm/index.js.map +1 -0
  43. package/dist/esm/package.json +3 -0
  44. package/dist/esm/polyfill.d.ts +17 -0
  45. package/dist/esm/polyfill.d.ts.map +1 -0
  46. package/dist/esm/polyfill.js +22 -0
  47. package/dist/esm/polyfill.js.map +1 -0
  48. package/dist/esm/subscribe.d.ts +39 -0
  49. package/dist/esm/subscribe.d.ts.map +1 -0
  50. package/dist/esm/subscribe.js +300 -0
  51. package/dist/esm/subscribe.js.map +1 -0
  52. package/dist/esm/version.d.ts +3 -0
  53. package/dist/esm/version.d.ts.map +1 -0
  54. package/dist/esm/version.js +3 -0
  55. package/dist/esm/version.js.map +1 -0
  56. package/package.json +44 -3
  57. package/src/__types__/typeUtils.ts +39 -0
  58. package/src/__types__/typesTests.ts +139 -0
  59. package/src/index.ts +1731 -0
  60. package/src/polyfill.ts +39 -0
  61. package/src/subscribe.ts +442 -0
  62. package/src/version.ts +3 -0
  63. package/tsconfig.dev.json +14 -0
  64. package/tsconfig.json +11 -0
  65. package/tsconfig.test.json +5 -0
package/src/index.ts ADDED
@@ -0,0 +1,1731 @@
1
+ import type { Readable } from 'node:stream';
2
+ import { validate as uuidValidate } from 'uuid';
3
+
4
+ import {
5
+ tx,
6
+ lookup,
7
+ getOps,
8
+ i,
9
+ id,
10
+ txInit,
11
+ version as coreVersion,
12
+ InstantAPIError,
13
+ setInstantWarningsEnabled,
14
+ type InstantIssue,
15
+ type TransactionChunk,
16
+ type AuthToken,
17
+
18
+ // core types
19
+ type User,
20
+ type Query,
21
+ InstantError,
22
+
23
+ // query types
24
+ type QueryResponse,
25
+ type InstaQLResponse,
26
+ type InstaQLParams,
27
+ type ValidQuery,
28
+ type InstaQLFields,
29
+ type InstantQuery,
30
+ type InstantQueryResult,
31
+ type InstantSchema,
32
+ type InstantSchemaDatabase,
33
+ type InstantObject,
34
+ type InstantEntity,
35
+ type BackwardsCompatibleSchema,
36
+ type IInstantDatabase,
37
+
38
+ // schema types
39
+ type AttrsDefs,
40
+ type CardinalityKind,
41
+ type DataAttrDef,
42
+ type EntitiesDef,
43
+ type EntitiesWithLinks,
44
+ type EntityDef,
45
+ type InstantGraph,
46
+ type LinkAttrDef,
47
+ type LinkDef,
48
+ type InstantUnknownSchemaDef,
49
+ type LinksDef,
50
+ type RoomsOf,
51
+ type RoomsDef,
52
+ type PresenceOf,
53
+ type RoomHandle,
54
+ type ResolveAttrs,
55
+ type ValueTypes,
56
+ type InstantSchemaDef,
57
+ type InstantUnknownSchema,
58
+ type InstaQLEntity,
59
+ type InstaQLResult,
60
+ type InstaQLEntitySubquery,
61
+ type InstantRules,
62
+ type UpdateParams,
63
+ type CreateParams,
64
+ type LinkParams,
65
+ type RuleParams,
66
+ type TopicsOf,
67
+ type TopicOf,
68
+
69
+ // storage types
70
+ type FileOpts,
71
+ type UploadFileResponse,
72
+ type DeleteFileResponse,
73
+ validateQuery,
74
+ validateTransactions,
75
+ createInstantRouteHandler,
76
+ type InstantRouteHandlerPayloadByType,
77
+ type InstantRouteHandlerType,
78
+ type InstantRouteHandlerBody,
79
+ SSEConnection,
80
+ InstantStream,
81
+ EventSourceConstructor,
82
+ EventSourceType,
83
+ type WritableStreamCtor,
84
+ type ReadableStreamCtor,
85
+ InstantWritableStream,
86
+ InstantReadableStream,
87
+ type Logger,
88
+ } from '@instantdb/core';
89
+
90
+ import version from './version.ts';
91
+
92
+ import {
93
+ subscribe,
94
+ SubscribeQueryCallback,
95
+ SubscribeQueryResponse,
96
+ SubscribeQueryPayload,
97
+ SubscriptionReadyState,
98
+ } from './subscribe.ts';
99
+ import { parseCookie } from 'cookie';
100
+ import { EventSource } from '@instantdb/eventsource';
101
+ import { MessageEventPolyfill } from './polyfill.ts';
102
+ import {
103
+ Webhooks,
104
+ WebhooksManager,
105
+ type WebhookAction,
106
+ type WebhookStatus,
107
+ type WebhookEventStatus,
108
+ type WebhookInfo,
109
+ type WebhookAttempt,
110
+ type WebhookEventInfo,
111
+ type WebhookEventsPage,
112
+ type WebhookBody,
113
+ type WebhookEntity,
114
+ type WebhookPayload,
115
+ type WebhookPayloadRecord,
116
+ type WebhookPayloadRecordFor,
117
+ type WebhookHandlerFn,
118
+ type WebhookHandlers,
119
+ type WebhookHelpers,
120
+ type CreateWebhookParams,
121
+ type UpdateWebhookParams,
122
+ } from '@instantdb/webhooks';
123
+
124
+ type DebugCheckResult = {
125
+ /** The ID of the record. */
126
+ id: string;
127
+ /** The namespace/table of the record. */
128
+ entity: string;
129
+ /** The value of the record. */
130
+ record: Record<string, any>;
131
+ /** The result of evaluating the corresponding permissions rule for a record. */
132
+ check: any;
133
+ };
134
+
135
+ type Config = {
136
+ appId: string;
137
+ adminToken?: string;
138
+ apiURI?: string;
139
+ useDateObjects?: boolean;
140
+ disableValidation?: boolean;
141
+ verbose?: boolean;
142
+ logger?: Logger;
143
+ };
144
+
145
+ export type InstantConfig<
146
+ Schema extends InstantSchemaDef<any, any, any>,
147
+ UseDates extends boolean = false,
148
+ > = {
149
+ appId: string;
150
+ adminToken?: string;
151
+ apiURI?: string;
152
+ schema?: Schema;
153
+ useDateObjects: UseDates;
154
+ disableValidation?: boolean;
155
+ verbose?: boolean;
156
+ logger?: Logger;
157
+ WritableStream?: WritableStreamCtor;
158
+ ReadableStream?: ReadableStreamCtor;
159
+ };
160
+
161
+ type InstantConfigFilled<
162
+ Schema extends InstantSchemaDef<any, any, any>,
163
+ UseDates extends boolean,
164
+ > = InstantConfig<Schema, UseDates> & { apiURI: string };
165
+
166
+ type FilledConfig = Config & { apiURI: string };
167
+
168
+ type ImpersonationOpts =
169
+ | { email: string }
170
+ | { token: AuthToken }
171
+ | { guest: boolean };
172
+
173
+ type DebugTransactResult = {
174
+ 'tx-id': number;
175
+ 'all-checks-ok?': boolean;
176
+ };
177
+
178
+ function configWithDefaults(config: Config): FilledConfig {
179
+ const defaultConfig = {
180
+ apiURI: 'https://api.instantdb.com',
181
+ };
182
+ const r = { ...defaultConfig, ...config };
183
+ return r;
184
+ }
185
+
186
+ function instantConfigWithDefaults<
187
+ Schema extends InstantSchemaDef<any, any, any>,
188
+ UseDates extends boolean,
189
+ >(
190
+ config: InstantConfig<Schema, UseDates>,
191
+ ): InstantConfigFilled<Schema, UseDates> {
192
+ const defaultConfig = {
193
+ apiURI: 'https://api.instantdb.com',
194
+ };
195
+ const r = { ...defaultConfig, ...config };
196
+ if (!r.apiURI) {
197
+ r.apiURI = defaultConfig.apiURI;
198
+ }
199
+ return r;
200
+ }
201
+
202
+ function withImpersonation(
203
+ headers: { [key: string]: string },
204
+ opts: ImpersonationOpts,
205
+ ) {
206
+ if ('email' in opts) {
207
+ headers['as-email'] = opts.email;
208
+ } else if ('token' in opts) {
209
+ headers['as-token'] = opts.token;
210
+ } else if ('guest' in opts) {
211
+ headers['as-guest'] = 'true';
212
+ }
213
+ return headers;
214
+ }
215
+
216
+ function validateConfigAndImpersonation(
217
+ config: FilledConfig,
218
+ impersonationOpts: ImpersonationOpts | undefined,
219
+ ) {
220
+ if (
221
+ impersonationOpts &&
222
+ ('token' in impersonationOpts || 'guest' in impersonationOpts)
223
+ ) {
224
+ // adminToken is not required for `token` or `guest` impersonation
225
+ return;
226
+ }
227
+ if (config.adminToken) {
228
+ // An adminToken is provided.
229
+ return;
230
+ }
231
+ if (impersonationOpts && 'email' in impersonationOpts) {
232
+ throw new Error(
233
+ 'Admin token required. To impersonate users with an email you must pass `adminToken` to `init`.',
234
+ );
235
+ }
236
+ throw new Error(
237
+ 'Admin token required. To run this operation pass `adminToken` to `init`, or use `db.asUser`.',
238
+ );
239
+ }
240
+
241
+ function authorizedHeaders(
242
+ config: FilledConfig,
243
+ impersonationOpts?: ImpersonationOpts,
244
+ ): Record<string, string> {
245
+ validateConfigAndImpersonation(config, impersonationOpts);
246
+
247
+ const { adminToken, appId } = config;
248
+ const headers: Record<string, string> = {
249
+ 'content-type': 'application/json',
250
+ 'app-id': appId,
251
+ };
252
+
253
+ if (adminToken) {
254
+ headers.authorization = `Bearer ${adminToken}`;
255
+ }
256
+
257
+ return impersonationOpts
258
+ ? withImpersonation(headers, impersonationOpts)
259
+ : headers;
260
+ }
261
+
262
+ // NextJS 13 and 14 cache fetch requests by default.
263
+ //
264
+ // Since adminDB.query uses fetch, this means that it would also cache by default.
265
+ //
266
+ // We don't want this behavior. `adminDB.query` should return the latest result by default.
267
+ //
268
+ // To get around this, we set an explicit `cache` header for NextJS 13 and 14.
269
+ // This is no longer needed in NextJS 15 onwards, as the default is `no-store` again.
270
+ // Once NextJS 13 and 14 are no longer common, we can remove this code.
271
+ function isNextJSVersionThatCachesFetchByDefault() {
272
+ return (
273
+ // NextJS 13 onwards added a `__nextPatched` property to the fetch function
274
+ fetch['__nextPatched'] &&
275
+ // NextJS 15 onwards _also_ added a global `next-patch` symbol.
276
+ !globalThis[Symbol.for('next-patch')]
277
+ );
278
+ }
279
+
280
+ function getDefaultFetchOpts(): RequestInit {
281
+ return isNextJSVersionThatCachesFetchByDefault() ? { cache: 'no-store' } : {};
282
+ }
283
+
284
+ async function jsonReject(rejectFn, res) {
285
+ const body = await res.text();
286
+ try {
287
+ const json = JSON.parse(body);
288
+ return rejectFn(new InstantAPIError({ status: res.status, body: json }));
289
+ } catch (_e) {
290
+ return rejectFn(
291
+ new InstantAPIError({
292
+ status: res.status,
293
+ body: { type: undefined, message: body },
294
+ }),
295
+ );
296
+ }
297
+ }
298
+
299
+ async function jsonFetch(
300
+ input: RequestInfo,
301
+ init: RequestInit | undefined,
302
+ ): Promise<any> {
303
+ const defaultFetchOpts = getDefaultFetchOpts();
304
+ const headers = {
305
+ ...(init?.headers || {}),
306
+ 'Instant-Admin-Version': version,
307
+ 'Instant-Core-Version': coreVersion,
308
+ };
309
+ const res = await fetch(input, { ...defaultFetchOpts, ...init, headers });
310
+ if (res.status === 200) {
311
+ const json = await res.json();
312
+ return Promise.resolve(json);
313
+ }
314
+
315
+ return jsonReject((x) => Promise.reject(x), res);
316
+ }
317
+
318
+ function makeEventSourceWrapper(opts: {
319
+ headers: HeadersInit;
320
+ inference: boolean;
321
+ }): EventSourceConstructor {
322
+ return class EventSourceWrapper {
323
+ source: EventSource;
324
+ static OPEN = EventSource.OPEN;
325
+ static CONNECTING = EventSource.CONNECTING;
326
+ static CLOSED = EventSource.CLOSED;
327
+ readonly url: string;
328
+
329
+ constructor(url: string) {
330
+ this.url = url;
331
+ this.source = this.#createEventSource(url);
332
+ }
333
+
334
+ get onopen(): EventSourceType['onopen'] {
335
+ return this.source.onopen;
336
+ }
337
+ set onopen(fn: EventSourceType['onopen']) {
338
+ this.source.onopen = fn;
339
+ }
340
+
341
+ get onmessage(): EventSourceType['onmessage'] {
342
+ return this.source.onmessage;
343
+ }
344
+ set onmessage(fn: EventSourceType['onmessage']) {
345
+ this.source.onmessage = fn;
346
+ }
347
+
348
+ get onerror(): EventSourceType['onerror'] {
349
+ return this.source.onerror;
350
+ }
351
+ set onerror(fn: EventSourceType['onerror']) {
352
+ this.source.onerror = fn;
353
+ }
354
+
355
+ public get readyState() {
356
+ return this.source.readyState;
357
+ }
358
+
359
+ public close() {
360
+ this.source.close();
361
+ }
362
+
363
+ #createEventSource(url: string): EventSource {
364
+ const es = new EventSource(url, {
365
+ messageEvent: MessageEventPolyfill,
366
+ fetch(input, init) {
367
+ return fetch(input, {
368
+ ...init,
369
+ method: 'POST',
370
+ headers: opts.headers,
371
+ body: JSON.stringify({
372
+ 'inference?': opts.inference,
373
+ versions: {
374
+ '@instantdb/admin': version,
375
+ '@instantdb/core': coreVersion,
376
+ },
377
+ }),
378
+ });
379
+ },
380
+ });
381
+ return es;
382
+ }
383
+ };
384
+ }
385
+
386
+ /**
387
+ *
388
+ * The first step: init your application!
389
+ *
390
+ * Visit https://instantdb.com/dash to get your `appId` :)
391
+ *
392
+ * @example
393
+ * import { init } from "@instantdb/admin"
394
+ *
395
+ * const db = init({
396
+ * appId: process.env.INSTANT_APP_ID!,
397
+ * adminToken: process.env.INSTANT_APP_ADMIN_TOKEN
398
+ * })
399
+ *
400
+ * // You can also provide a schema for type safety and editor autocomplete!
401
+ *
402
+ * import { init } from "@instantdb/admin"
403
+ * import schema from ""../instant.schema.ts";
404
+ *
405
+ * const db = init({
406
+ * appId: process.env.INSTANT_APP_ID!,
407
+ * adminToken: process.env.INSTANT_APP_ADMIN_TOKEN,
408
+ * schema,
409
+ * })
410
+ * // To learn more: https://instantdb.com/docs/modeling-data
411
+ */
412
+ function init<
413
+ Schema extends InstantSchemaDef<any, any, any> = InstantUnknownSchema,
414
+ UseDates extends boolean = false,
415
+ >(
416
+ // Allows config with missing `useDateObjects`, but keeps `UseDates`
417
+ // as a non-nullable in the InstantConfig type.
418
+ config: Omit<InstantConfig<Schema, UseDates>, 'useDateObjects'> & {
419
+ useDateObjects?: UseDates;
420
+ },
421
+ ): InstantAdminDatabase<Schema, UseDates, InstantConfig<Schema, UseDates>> {
422
+ if (!config.appId || !uuidValidate(config.appId)) {
423
+ console.warn(
424
+ 'warning: Instant Admin DB must be initialized with a valid appId. Received: ' +
425
+ JSON.stringify(config.appId),
426
+ );
427
+ }
428
+
429
+ const configStrict = {
430
+ ...config,
431
+ appId: config.appId?.trim(),
432
+ adminToken: config.adminToken?.trim(),
433
+ useDateObjects: (config.useDateObjects ?? false) as UseDates,
434
+ };
435
+
436
+ return new InstantAdminDatabase<
437
+ Schema,
438
+ UseDates,
439
+ InstantConfig<Schema, UseDates>
440
+ >(configStrict);
441
+ }
442
+
443
+ /**
444
+ * @deprecated
445
+ * `init_experimental` is deprecated. You can replace it with `init`.
446
+ *
447
+ * @example
448
+ *
449
+ * // Before
450
+ * import { init_experimental } from "@instantdb/admin"
451
+ * const db = init_experimental({ ... });
452
+ *
453
+ * // After
454
+ * import { init } from "@instantdb/admin"
455
+ * const db = init({ ... });
456
+ */
457
+ const init_experimental = init;
458
+
459
+ function steps(inputChunks) {
460
+ const chunks = Array.isArray(inputChunks) ? inputChunks : [inputChunks];
461
+ return chunks.flatMap(getOps);
462
+ }
463
+
464
+ type PresenceResult<Data> = {
465
+ [peerId: string]: { data: Data; 'peer-id': string; user: User | null };
466
+ };
467
+
468
+ class Rooms<Schema extends InstantSchemaDef<any, any, any>> {
469
+ config: FilledConfig;
470
+
471
+ constructor(config: FilledConfig) {
472
+ this.config = config;
473
+ }
474
+
475
+ async getPresence<RoomType extends string & keyof RoomsOf<Schema>>(
476
+ roomType: RoomType,
477
+ roomId: string,
478
+ ): Promise<PresenceResult<PresenceOf<Schema, RoomType>>> {
479
+ const res = await jsonFetch(
480
+ `${this.config.apiURI}/admin/rooms/presence?app_id=${this.config.appId}&room-type=${String(roomType)}&room-id=${roomId}`,
481
+ {
482
+ method: 'GET',
483
+ headers: authorizedHeaders(this.config),
484
+ },
485
+ );
486
+
487
+ return res.sessions || {};
488
+ }
489
+ }
490
+
491
+ class Auth<Schema extends InstantSchemaDef<any, any, any>> {
492
+ config: FilledConfig;
493
+
494
+ constructor(config: FilledConfig) {
495
+ this.config = config;
496
+ this.createToken = this.createToken.bind(this);
497
+ }
498
+
499
+ /**
500
+ * Generates a magic code for the user with the given email.
501
+ * This is useful if you want to use your own email provider
502
+ * to send magic codes.
503
+ *
504
+ * @example
505
+ * // Generate a magic code
506
+ * const { code } = await db.auth.generateMagicCode({ email })
507
+ * // Send the magic code to the user with your own email provider
508
+ * await customEmailProvider.sendMagicCode(email, code)
509
+ *
510
+ * @see https://instantdb.com/docs/backend#custom-magic-codes
511
+ */
512
+ generateMagicCode = async (email: string): Promise<{ code: string }> => {
513
+ return jsonFetch(
514
+ `${this.config.apiURI}/admin/magic_code?app_id=${this.config.appId}`,
515
+ {
516
+ method: 'POST',
517
+ headers: authorizedHeaders(this.config),
518
+ body: JSON.stringify({ email }),
519
+ },
520
+ );
521
+ };
522
+
523
+ /**
524
+ * Sends a magic code to the user with the given email.
525
+ * This uses Instant's built-in email provider.
526
+ *
527
+ * @example
528
+ * // Send an email to user with magic code
529
+ * await db.auth.sendMagicCode({ email })
530
+ *
531
+ * @see https://instantdb.com/docs/backend#custom-magic-codes
532
+ */
533
+ sendMagicCode = async (email: string): Promise<{ code: string }> => {
534
+ return jsonFetch(
535
+ `${this.config.apiURI}/admin/send_magic_code?app_id=${this.config.appId}`,
536
+ {
537
+ method: 'POST',
538
+ headers: authorizedHeaders(this.config),
539
+ body: JSON.stringify({ email }),
540
+ },
541
+ );
542
+ };
543
+
544
+ /**
545
+ * @deprecated Use {@link checkMagicCode} instead to get the `created` field
546
+ * and support `extraFields`.
547
+ *
548
+ * @see https://instantdb.com/docs/backend#custom-magic-codes
549
+ */
550
+ verifyMagicCode = async (email: string, code: string): Promise<User> => {
551
+ const { user } = await jsonFetch(
552
+ `${this.config.apiURI}/admin/verify_magic_code?app_id=${this.config.appId}`,
553
+ {
554
+ method: 'POST',
555
+ headers: authorizedHeaders(this.config),
556
+ body: JSON.stringify({ email, code }),
557
+ },
558
+ );
559
+ return user;
560
+ };
561
+
562
+ /**
563
+ * Verifies a magic code and returns the user along with whether
564
+ * the user was newly created. Supports `extraFields` to set custom
565
+ * `$users` properties at signup.
566
+ *
567
+ * @example
568
+ * const { user, created } = await db.auth.checkMagicCode(
569
+ * email,
570
+ * code,
571
+ * { extraFields: { nickname: 'ari' } },
572
+ * );
573
+ *
574
+ * @see https://instantdb.com/docs/backend#custom-magic-codes
575
+ */
576
+ checkMagicCode = async (
577
+ email: string,
578
+ code: string,
579
+ options?: { extraFields?: UpdateParams<Schema, '$users'> },
580
+ ): Promise<{ user: User; created: boolean }> => {
581
+ const res = await jsonFetch(
582
+ `${this.config.apiURI}/admin/verify_magic_code?app_id=${this.config.appId}`,
583
+ {
584
+ method: 'POST',
585
+ headers: authorizedHeaders(this.config),
586
+ body: JSON.stringify({
587
+ email,
588
+ code,
589
+ ...(options?.extraFields
590
+ ? { 'extra-fields': options.extraFields }
591
+ : {}),
592
+ }),
593
+ },
594
+ );
595
+ return { user: res.user, created: res.created };
596
+ };
597
+
598
+ /**
599
+ * Creates a login token for the user with the given email.
600
+ * If that user does not exist, we create one.
601
+ *
602
+ * This is often useful for writing custom auth flows.
603
+ *
604
+ * @example
605
+ * app.post('/custom_sign_in', async (req, res) => {
606
+ * // ... your custom flow for users
607
+ * const token = await db.auth.createToken({ email })
608
+ * return res.status(200).send({ token })
609
+ * })
610
+ *
611
+ * @see https://instantdb.com/docs/backend#custom-auth
612
+ */
613
+ createToken(email: { email: string } | { id: string }): Promise<AuthToken>;
614
+
615
+ /**
616
+ *
617
+ * @deprecated Passing an email string directly is deprecated.
618
+ *
619
+ * Use an object with the `email` key instead.
620
+ *
621
+ * @example
622
+ * // Before
623
+ * const token = await auth.createToken(email)
624
+ * // After
625
+ * const token = await auth.createToken({ email })
626
+ */
627
+ createToken(email: string): Promise<AuthToken>;
628
+
629
+ async createToken(
630
+ input: string | { email: string } | { id: string },
631
+ ): Promise<AuthToken> {
632
+ const body = typeof input === 'string' ? { email: input } : input;
633
+ const ret: { user: { refresh_token: string } } = await jsonFetch(
634
+ `${this.config.apiURI}/admin/refresh_tokens?app_id=${this.config.appId}`,
635
+ {
636
+ method: 'POST',
637
+ headers: authorizedHeaders(this.config),
638
+ body: JSON.stringify(body),
639
+ },
640
+ );
641
+ return ret.user.refresh_token;
642
+ }
643
+
644
+ /**
645
+ * Verifies a given token and returns the associated user.
646
+ *
647
+ * This is often useful for writing custom endpoints, where you need
648
+ * to authenticate users.
649
+ *
650
+ * @example
651
+ * app.post('/custom_endpoint', async (req, res) => {
652
+ * const user = await db.auth.verifyToken(req.headers['token'])
653
+ * if (!user) {
654
+ * return res.status(401).send('Uh oh, you are not authenticated')
655
+ * }
656
+ * // ...
657
+ * })
658
+ * @see https://instantdb.com/docs/backend#custom-endpoints
659
+ */
660
+ verifyToken = async (token: AuthToken): Promise<User> => {
661
+ const res = await jsonFetch(
662
+ `${this.config.apiURI}/runtime/auth/verify_refresh_token?app_id=${this.config.appId}`,
663
+ {
664
+ method: 'POST',
665
+ headers: { 'content-type': 'application/json' },
666
+ body: JSON.stringify({
667
+ 'app-id': this.config.appId,
668
+ 'refresh-token': token,
669
+ }),
670
+ },
671
+ );
672
+ return res.user;
673
+ };
674
+
675
+ /**
676
+ * Retrieves an app user by id, email, or refresh token.
677
+ * Resolves to `null` when no user matches; throws on malformed
678
+ * input or auth errors.
679
+ *
680
+ * @example
681
+ * const user = await db.auth.getUser({ email });
682
+ * if (!user) {
683
+ * console.log("No user found");
684
+ * return;
685
+ * }
686
+ * console.log("Found user:", user);
687
+ *
688
+ * @see https://instantdb.com/docs/backend#retrieve-a-user
689
+ */
690
+ getUser = async (
691
+ params: { email: string } | { id: string } | { refresh_token: string },
692
+ ): Promise<Omit<User, 'refresh_token'> | null> => {
693
+ const qs = new URLSearchParams(Object.entries(params)).toString();
694
+
695
+ const response: { user: User | null } = await jsonFetch(
696
+ `${this.config.apiURI}/admin/users?app_id=${this.config.appId}&${qs}`,
697
+ {
698
+ method: 'GET',
699
+ headers: authorizedHeaders(this.config),
700
+ },
701
+ );
702
+
703
+ return response.user;
704
+ };
705
+
706
+ /**
707
+ * Deletes an app user by id, email, or refresh token.
708
+ * Resolves to `null` when no user matches; throws on malformed
709
+ * input or auth errors.
710
+ *
711
+ * NB: This _only_ deletes the user; it does not delete all user data.
712
+ * You will need to handle this manually.
713
+ *
714
+ * @example
715
+ * const deletedUser = await db.auth.deleteUser({ email });
716
+ * if (!deletedUser) {
717
+ * console.log("No user found to delete");
718
+ * return;
719
+ * }
720
+ * console.log("Deleted user:", deletedUser);
721
+ *
722
+ * @see https://instantdb.com/docs/backend#delete-a-user
723
+ */
724
+ deleteUser = async (
725
+ params: { email: string } | { id: string } | { refresh_token: string },
726
+ ): Promise<User | null> => {
727
+ const qs = new URLSearchParams(Object.entries(params)).toString();
728
+ const response: { deleted: User | null } = await jsonFetch(
729
+ `${this.config.apiURI}/admin/users?app_id=${this.config.appId}&${qs}`,
730
+ {
731
+ method: 'DELETE',
732
+ headers: authorizedHeaders(this.config),
733
+ },
734
+ );
735
+
736
+ return response.deleted;
737
+ };
738
+
739
+ /**
740
+ * Signs out the user with the given email.
741
+ * This invalidates any tokens associated with the user.
742
+ * If the user is not found, an error will be thrown.
743
+ *
744
+ * @example
745
+ * try {
746
+ * await auth.signOut({ email: "alyssa_p_hacker@instantdb.com" });
747
+ * console.log("Successfully signed out");
748
+ * } catch (err) {
749
+ * console.error("Sign out failed:", err.message);
750
+ * }
751
+ *
752
+ * @see https://instantdb.com/docs/backend#sign-out
753
+ */
754
+ async signOut(
755
+ params: { email: string } | { id: string } | { refresh_token: string },
756
+ ): Promise<void>;
757
+
758
+ /**
759
+ * @deprecated Passing an email string directly is deprecated.
760
+ * Use an object with the `email` key instead.
761
+ *
762
+ * @example
763
+ * // Before
764
+ * auth.signOut(email)
765
+ *
766
+ * // After
767
+ * auth.signOut({ email })
768
+ */
769
+ async signOut(email: string): Promise<void>;
770
+
771
+ async signOut(
772
+ input:
773
+ | string
774
+ | { email: string }
775
+ | { id: string }
776
+ | { refresh_token: string },
777
+ ): Promise<void> {
778
+ // If input is a string, we assume it's an email.
779
+ // This is because of backwards compatibility: we used to only
780
+ // accept email strings. Eventually we can remove this
781
+ const params = typeof input === 'string' ? { email: input } : input;
782
+ const config = this.config;
783
+ await jsonFetch(
784
+ `${config.apiURI}/admin/sign_out?app_id=${this.config.appId}`,
785
+ {
786
+ method: 'POST',
787
+ headers: authorizedHeaders(config),
788
+ body: JSON.stringify(params),
789
+ },
790
+ );
791
+ }
792
+
793
+ /**
794
+ * Get instant user from Request
795
+ *
796
+ * Reads cookies and gets a validated user
797
+ * @param req The request containing a cookie synced with createInstantRouteHandler
798
+ * @param opts Allow disabling validation of refresh token
799
+ */
800
+ getUserFromRequest = async (
801
+ req: Request,
802
+ opts?: { disableValidation?: boolean },
803
+ ): Promise<User | null> => {
804
+ const cookieHeader = req.headers.get('cookie') || '';
805
+
806
+ const parsedCookie = parseCookie(cookieHeader);
807
+
808
+ const cookieName = 'instant_user_' + this.config.appId;
809
+ if (!parsedCookie[cookieName]) {
810
+ return null;
811
+ }
812
+ const value = parsedCookie[cookieName];
813
+ const user = JSON.parse(value);
814
+ if (!user?.refresh_token) {
815
+ return null;
816
+ }
817
+ if (opts?.disableValidation) {
818
+ return user;
819
+ }
820
+ const verified = await this.verifyToken(user.refresh_token);
821
+ return verified;
822
+ };
823
+ }
824
+
825
+ type StorageFile = {
826
+ key: string;
827
+ name: string;
828
+ size: number;
829
+ etag: string;
830
+ last_modified: number;
831
+ };
832
+
833
+ type DeleteManyFileResponse = {
834
+ data: {
835
+ ids: string[] | null;
836
+ };
837
+ };
838
+
839
+ const isNodeReadable = (v: any): v is Readable =>
840
+ v &&
841
+ typeof v === 'object' &&
842
+ typeof v.pipe === 'function' &&
843
+ typeof v.read === 'function';
844
+
845
+ const isWebReadable = (v: any): v is ReadableStream =>
846
+ v && typeof v.getReader === 'function';
847
+
848
+ /**
849
+ * Functions to manage file storage.
850
+ */
851
+ class Storage {
852
+ config: FilledConfig;
853
+ impersonationOpts?: ImpersonationOpts;
854
+
855
+ constructor(config: FilledConfig, impersonationOpts?: ImpersonationOpts) {
856
+ this.config = config;
857
+ this.impersonationOpts = impersonationOpts;
858
+ }
859
+
860
+ /**
861
+ * Uploads file at the provided path. Accepts a Buffer or a Readable stream.
862
+ *
863
+ * @see https://instantdb.com/docs/storage
864
+ * @example
865
+ * const buffer = fs.readFileSync('demo.png');
866
+ * const isSuccess = await db.storage.uploadFile('photos/demo.png', buffer);
867
+ */
868
+ uploadFile = async (
869
+ path: string,
870
+ file: Buffer | Readable | ReadableStream | Uint8Array,
871
+ metadata: FileOpts = {},
872
+ ): Promise<UploadFileResponse> => {
873
+ const headers = {
874
+ ...authorizedHeaders(this.config, this.impersonationOpts),
875
+ path,
876
+ };
877
+ if (metadata.contentDisposition) {
878
+ headers['content-disposition'] = metadata.contentDisposition;
879
+ }
880
+
881
+ // headers.content-type will become "undefined" (string)
882
+ // if not removed from the object
883
+ delete headers['content-type'];
884
+
885
+ if (metadata.contentType) {
886
+ headers['content-type'] = metadata.contentType;
887
+ }
888
+
889
+ let duplex: 'half' | undefined;
890
+ if (isNodeReadable(file)) {
891
+ duplex = 'half'; // one-way stream
892
+ }
893
+ if (isNodeReadable(file) || isWebReadable(file)) {
894
+ if (!metadata.fileSize) {
895
+ throw new Error(
896
+ 'fileSize is required in metadata when uploading streams',
897
+ );
898
+ }
899
+ headers['content-length'] = metadata.fileSize.toString();
900
+ }
901
+
902
+ let options = {
903
+ method: 'PUT',
904
+ headers,
905
+ body: file as unknown as BodyInit,
906
+ ...(duplex && { duplex }),
907
+ };
908
+
909
+ return jsonFetch(
910
+ `${this.config.apiURI}/admin/storage/upload?app_id=${this.config.appId}`,
911
+ options,
912
+ );
913
+ };
914
+
915
+ /**
916
+ * Deletes a file by its path name (e.g. "photos/demo.png").
917
+ *
918
+ * @deprecated Use `db.transact` to delete files instead:
919
+ * @example
920
+ * // Delete by id
921
+ * await db.transact(db.tx.$files[fileId].delete());
922
+ *
923
+ * // Delete by path
924
+ * await db.transact(db.tx.$files[lookup('path', 'photos/demo.png')].delete());
925
+ *
926
+ * @see https://instantdb.com/docs/storage
927
+ */
928
+ delete = async (pathname: string): Promise<DeleteFileResponse> => {
929
+ return jsonFetch(
930
+ `${this.config.apiURI}/admin/storage/files?app_id=${this.config.appId}&filename=${encodeURIComponent(
931
+ pathname,
932
+ )}`,
933
+ {
934
+ method: 'DELETE',
935
+ headers: authorizedHeaders(this.config, this.impersonationOpts),
936
+ },
937
+ );
938
+ };
939
+
940
+ /**
941
+ * Deletes multiple files by their path names.
942
+ *
943
+ * @deprecated Use `db.transact` to delete files instead:
944
+ * @example
945
+ * // Delete multiple files by path
946
+ * const paths = ['images/1.png', 'images/2.png', 'images/3.png'];
947
+ * await db.transact(paths.map(p => db.tx.$files[lookup('path', p)].delete()));
948
+ *
949
+ * @see https://instantdb.com/docs/storage
950
+ */
951
+ deleteMany = async (pathnames: string[]): Promise<DeleteManyFileResponse> => {
952
+ return jsonFetch(
953
+ `${this.config.apiURI}/admin/storage/files/delete?app_id=${this.config.appId}`,
954
+ {
955
+ method: 'POST',
956
+ headers: authorizedHeaders(this.config, this.impersonationOpts),
957
+ body: JSON.stringify({ filenames: pathnames }),
958
+ },
959
+ );
960
+ };
961
+
962
+ /**
963
+ * @deprecated. This method will be removed in the future. Use `uploadFile`
964
+ * instead
965
+ */
966
+ upload = async (
967
+ pathname: string,
968
+ file: Buffer,
969
+ metadata: FileOpts = {},
970
+ ): Promise<boolean> => {
971
+ const { data: presignedUrl } = await jsonFetch(
972
+ `${this.config.apiURI}/admin/storage/signed-upload-url?app_id=${this.config.appId}`,
973
+ {
974
+ method: 'POST',
975
+ headers: authorizedHeaders(this.config),
976
+ body: JSON.stringify({
977
+ app_id: this.config.appId,
978
+ filename: pathname,
979
+ }),
980
+ },
981
+ );
982
+ const headers = {};
983
+ const contentType = metadata.contentType;
984
+ if (contentType) {
985
+ headers['Content-Type'] = contentType;
986
+ }
987
+ const { ok } = await fetch(presignedUrl, {
988
+ method: 'PUT',
989
+ body: file as unknown as BodyInit,
990
+ headers,
991
+ });
992
+
993
+ return ok;
994
+ };
995
+
996
+ /**
997
+ * @deprecated. This method will be removed in the future. Use `query` instead
998
+ * @example
999
+ * const files = await db.query({ $files: {}})
1000
+ */
1001
+ list = async (): Promise<StorageFile[]> => {
1002
+ const { data } = await jsonFetch(
1003
+ `${this.config.apiURI}/admin/storage/files?app_id=${this.config.appId}`,
1004
+ {
1005
+ method: 'GET',
1006
+ headers: authorizedHeaders(this.config),
1007
+ },
1008
+ );
1009
+
1010
+ return data;
1011
+ };
1012
+
1013
+ /**
1014
+ * @deprecated. getDownloadUrl will be removed in the future.
1015
+ * Use `query` instead to query and fetch for valid urls
1016
+ *
1017
+ * db.useQuery({
1018
+ * $files: {
1019
+ * $: {
1020
+ * where: {
1021
+ * path: "moop.png"
1022
+ * }
1023
+ * }
1024
+ * }
1025
+ * })
1026
+ */
1027
+ getDownloadUrl = async (pathname: string): Promise<string> => {
1028
+ const { data } = await jsonFetch(
1029
+ `${this.config.apiURI}/admin/storage/signed-download-url?app_id=${this.config.appId}&filename=${encodeURIComponent(pathname)}`,
1030
+ {
1031
+ method: 'GET',
1032
+ headers: authorizedHeaders(this.config),
1033
+ },
1034
+ );
1035
+
1036
+ return data;
1037
+ };
1038
+ }
1039
+
1040
+ type CreateReadStreamOpts = {
1041
+ clientId?: string | null | undefined;
1042
+ streamId?: string | null | undefined;
1043
+ byteOffset?: number | null | undefined;
1044
+ ruleParams?: RuleParams | null | undefined;
1045
+ };
1046
+
1047
+ type CreateWriteStreamOpts = {
1048
+ clientId: string;
1049
+ /**
1050
+ * A function that takes a promise and ensures that the current program stays alive
1051
+ * until the promise is resolved.
1052
+ *
1053
+ * Useful in serverless environments to ensure the writer fully flushes before the
1054
+ * environment is shut down.
1055
+ *
1056
+ * @example
1057
+ * import { after } from 'next/server'
1058
+ * db.streams.createWriteStream({clientId, waitUntil: after})
1059
+ */
1060
+ waitUntil?: (promise: Promise<any>) => void | null | undefined;
1061
+ ruleParams?: RuleParams | null | undefined;
1062
+ };
1063
+
1064
+ /**
1065
+ * Functions to manage streams.
1066
+ */
1067
+ class Streams {
1068
+ #ensureInstantStream: () => InstantStream;
1069
+
1070
+ constructor(ensureInstantStream: () => InstantStream) {
1071
+ this.#ensureInstantStream = ensureInstantStream;
1072
+ }
1073
+
1074
+ /**
1075
+ * Creates a new ReadableStream for the given clientId.
1076
+ *
1077
+ * @example
1078
+ * const stream = db.streams.createReadStream({clientId: clientId})
1079
+ * for await (const chunk of stream) {
1080
+ * console.log(chunk);
1081
+ * }
1082
+ */
1083
+ createReadStream = (
1084
+ opts: CreateReadStreamOpts,
1085
+ ): InstantReadableStream<string> => {
1086
+ return this.#ensureInstantStream().createReadStream(opts);
1087
+ };
1088
+
1089
+ /**
1090
+ * Creates a new WritableStream for the given clientId.
1091
+ *
1092
+ * @example
1093
+ * const writeStream = db.streams.createWriteStream({clientId: clientId})
1094
+ * const writer = writeStream.getWriter();
1095
+ * writer.write('Hello world');
1096
+ * writer.close();
1097
+ */
1098
+ createWriteStream = (
1099
+ opts: CreateWriteStreamOpts,
1100
+ ): InstantWritableStream<string> => {
1101
+ return this.#ensureInstantStream().createWriteStream(opts);
1102
+ };
1103
+ }
1104
+
1105
+ type AdminQueryOpts = {
1106
+ ruleParams?: RuleParams;
1107
+ fetchOpts?: RequestInit;
1108
+ };
1109
+
1110
+ function createLogger(
1111
+ isEnabled: boolean,
1112
+ baseLogger: Logger = console,
1113
+ ): Logger {
1114
+ return {
1115
+ info: isEnabled ? (...args: any[]) => baseLogger.info(...args) : () => {},
1116
+ debug: isEnabled ? (...args: any[]) => baseLogger.debug(...args) : () => {},
1117
+ error: isEnabled ? (...args: any[]) => baseLogger.error(...args) : () => {},
1118
+ };
1119
+ }
1120
+
1121
+ /**
1122
+ *
1123
+ * The first step: init your application!
1124
+ *
1125
+ * Visit https://instantdb.com/dash to get your `appId` and `adminToken` :)
1126
+ *
1127
+ * @example
1128
+ * const db = init({ appId: "my-app-id", adminToken: "my-admin-token" })
1129
+ */
1130
+ class InstantAdminDatabase<
1131
+ Schema extends InstantSchemaDef<any, any, any>,
1132
+ UseDates extends boolean = false,
1133
+ Config extends InstantConfig<Schema, UseDates> = InstantConfig<
1134
+ Schema,
1135
+ UseDates
1136
+ >,
1137
+ > {
1138
+ config: InstantConfigFilled<Schema, UseDates>;
1139
+ auth: Auth<Schema>;
1140
+ storage: Storage;
1141
+ streams: Streams;
1142
+ rooms: Rooms<Schema>;
1143
+ impersonationOpts?: ImpersonationOpts;
1144
+ webhooks: Webhooks<Schema>;
1145
+
1146
+ #sseConnection: SSEConnection | null = null;
1147
+ #sseBackoff = 0;
1148
+ #instantStream: InstantStream | null = null;
1149
+ #log: Logger;
1150
+
1151
+ public tx = txInit<NonNullable<Schema>>();
1152
+
1153
+ constructor(_config: Config) {
1154
+ this.config = instantConfigWithDefaults(_config);
1155
+ this.auth = new Auth<Schema>(this.config);
1156
+ this.storage = new Storage(this.config, this.impersonationOpts);
1157
+ this.streams = new Streams(this.#ensureInstantStream.bind(this));
1158
+ this.rooms = new Rooms<Schema>(this.config);
1159
+ this.webhooks = new Webhooks<Schema>(this.config, jsonFetch);
1160
+ this.#log = createLogger(!!this.config.verbose, this.config.logger);
1161
+ }
1162
+
1163
+ /**
1164
+ * Sometimes you want to scope queries to a specific user.
1165
+ *
1166
+ * You can provide a user's auth token, email, or impersonate a guest.
1167
+ *
1168
+ * @see https://instantdb.com/docs/backend#impersonating-users
1169
+ * @example
1170
+ * await db.asUser({email: "stopa@instantdb.com"}).query({ goals: {} })
1171
+ */
1172
+ asUser = (
1173
+ opts: ImpersonationOpts,
1174
+ ): InstantAdminDatabase<Schema, UseDates, Config> => {
1175
+ const newClient = new InstantAdminDatabase<Schema, UseDates, Config>({
1176
+ ...(this.config as Config),
1177
+ });
1178
+ newClient.impersonationOpts = opts;
1179
+ newClient.storage = new Storage(this.config, opts);
1180
+ return newClient;
1181
+ };
1182
+
1183
+ /**
1184
+ * Use this to query your data!
1185
+ *
1186
+ * @see https://instantdb.com/docs/instaql
1187
+ *
1188
+ * @example
1189
+ * // fetch all goals
1190
+ * await db.query({ goals: {} })
1191
+ *
1192
+ * // goals where the title is "Get Fit"
1193
+ * await db.query({ goals: { $: { where: { title: "Get Fit" } } } })
1194
+ *
1195
+ * // all goals, _alongside_ their todos
1196
+ * await db.query({ goals: { todos: {} } })
1197
+ */
1198
+ query = <Q extends ValidQuery<Q, Schema>>(
1199
+ query: Q,
1200
+ opts: AdminQueryOpts = {},
1201
+ ): Promise<InstaQLResponse<Schema, Q, UseDates>> => {
1202
+ if (query && opts && 'ruleParams' in opts) {
1203
+ query = { $$ruleParams: opts['ruleParams'], ...query };
1204
+ }
1205
+
1206
+ if (!this.config.disableValidation) {
1207
+ validateQuery(query, this.config.schema);
1208
+ }
1209
+
1210
+ const fetchOpts = opts.fetchOpts || {};
1211
+ const fetchOptsHeaders = fetchOpts['headers'] || {};
1212
+ return jsonFetch(
1213
+ `${this.config.apiURI}/admin/query?app_id=${this.config.appId}`,
1214
+ {
1215
+ ...fetchOpts,
1216
+ method: 'POST',
1217
+ headers: {
1218
+ ...fetchOptsHeaders,
1219
+ ...authorizedHeaders(this.config, this.impersonationOpts),
1220
+ },
1221
+ body: JSON.stringify({
1222
+ query: query,
1223
+ 'inference?': !!this.config.schema,
1224
+ }),
1225
+ },
1226
+ );
1227
+ };
1228
+
1229
+ /**
1230
+ * Use this to to get a live view of your data!
1231
+ *
1232
+ * @see https://www.instantdb.com/docs/backend
1233
+ *
1234
+ * @example
1235
+ * // create a subscription to a query
1236
+ * const query = { goals: { $: { where: { title: "Get Fit" } } } }
1237
+ * const sub = db.subscribeQuery(query);
1238
+ *
1239
+ * // iterate through the results with an async iterator
1240
+ * for await (const payload of sub) {
1241
+ * if (payload.error) {
1242
+ * console.log(payload.error);
1243
+ * // Stop the subscription
1244
+ * sub.close();
1245
+ * } else {
1246
+ * console.log(payload.data);
1247
+ * }
1248
+ * }
1249
+ *
1250
+ * // Stop the subscription
1251
+ * sub.close();
1252
+ *
1253
+ * // Create a subscription with a callback
1254
+ * const sub = db.subscribeQuery(query, (payload) => {
1255
+ * if (payload.error) {
1256
+ * console.log(payload.error);
1257
+ * // Stop the subscription
1258
+ * sub.close();
1259
+ * } else {
1260
+ * console.log(payload.data);
1261
+ * }
1262
+ * });
1263
+ */
1264
+ subscribeQuery<Q extends ValidQuery<Q, Schema>>(
1265
+ query: Q,
1266
+ cb?: SubscribeQueryCallback<Schema, Q, UseDates>,
1267
+ opts: AdminQueryOpts = {},
1268
+ ): SubscribeQueryResponse<Schema, Q, UseDates> {
1269
+ if (query && opts && 'ruleParams' in opts) {
1270
+ query = { $$ruleParams: opts['ruleParams'], ...query };
1271
+ }
1272
+
1273
+ if (!this.config.disableValidation) {
1274
+ validateQuery(query, this.config.schema);
1275
+ }
1276
+
1277
+ const fetchOpts = opts.fetchOpts || {};
1278
+ const fetchOptsHeaders = fetchOpts['headers'] || {};
1279
+
1280
+ const headers: HeadersInit = {
1281
+ ...fetchOptsHeaders,
1282
+ ...authorizedHeaders(this.config, this.impersonationOpts),
1283
+ };
1284
+
1285
+ const inference = !!this.config.schema;
1286
+
1287
+ return subscribe(query, cb, {
1288
+ headers,
1289
+ inference,
1290
+ apiURI: this.config.apiURI,
1291
+ });
1292
+ }
1293
+
1294
+ /**
1295
+ * Use this to write data! You can create, update, delete, and link objects
1296
+ *
1297
+ * @see https://instantdb.com/docs/instaml
1298
+ *
1299
+ * @example
1300
+ * // Create a new object in the `goals` namespace
1301
+ * const goalId = id();
1302
+ * db.transact(db.tx.goals[goalId].update({title: "Get fit"}))
1303
+ *
1304
+ * // Update the title
1305
+ * db.transact(db.tx.goals[goalId].update({title: "Get super fit"}))
1306
+ *
1307
+ * // Delete it
1308
+ * db.transact(db.tx.goals[goalId].delete())
1309
+ *
1310
+ * // Or create an association:
1311
+ * todoId = id();
1312
+ * db.transact([
1313
+ * db.tx.todos[todoId].update({ title: 'Go on a run' }),
1314
+ * db.tx.goals[goalId].link({todos: todoId}),
1315
+ * ])
1316
+ */
1317
+ transact = (
1318
+ inputChunks: TransactionChunk<any, any> | TransactionChunk<any, any>[],
1319
+ ) => {
1320
+ if (!this.config.disableValidation) {
1321
+ validateTransactions(inputChunks, this.config.schema);
1322
+ }
1323
+ return jsonFetch(
1324
+ `${this.config.apiURI}/admin/transact?app_id=${this.config.appId}`,
1325
+ {
1326
+ method: 'POST',
1327
+ headers: authorizedHeaders(this.config, this.impersonationOpts),
1328
+ body: JSON.stringify({
1329
+ steps: steps(inputChunks),
1330
+ 'throw-on-missing-attrs?': !!this.config.schema,
1331
+ }),
1332
+ },
1333
+ );
1334
+ };
1335
+
1336
+ /**
1337
+ * Like `query`, but returns debugging information
1338
+ * for permissions checks along with the result.
1339
+ * Useful for inspecting the values returned by the permissions checks.
1340
+ * Note, this will return debug information for *all* entities
1341
+ * that match the query's `where` clauses.
1342
+ *
1343
+ * Requires a user/guest context to be set with `asUser`,
1344
+ * since permissions checks are user-specific.
1345
+ *
1346
+ * Accepts an optional configuration object with a `rules` key.
1347
+ * The provided rules will override the rules in the database for the query.
1348
+ *
1349
+ * @see https://instantdb.com/docs/instaql
1350
+ *
1351
+ * @example
1352
+ * await db.asUser({ guest: true }).debugQuery(
1353
+ * { goals: {} },
1354
+ * { rules: { goals: { allow: { read: "auth.id != null" } } }
1355
+ * )
1356
+ */
1357
+ debugQuery = async <Q extends ValidQuery<Q, Schema>>(
1358
+ query: Q,
1359
+ opts?: {
1360
+ rules?: any;
1361
+ ruleParams?: { [key: string]: any };
1362
+ ip?: string | null | undefined;
1363
+ origin?: string | null | undefined;
1364
+ cardinalityInference?: boolean;
1365
+ },
1366
+ ): Promise<{
1367
+ result: InstaQLResponse<Schema, Q, UseDates>;
1368
+ checkResults: DebugCheckResult[];
1369
+ }> => {
1370
+ if (query && opts && 'ruleParams' in opts) {
1371
+ query = { $$ruleParams: opts['ruleParams'], ...query };
1372
+ }
1373
+
1374
+ const body: any = {
1375
+ query,
1376
+ 'rules-override': opts?.rules,
1377
+ 'inference?': opts?.cardinalityInference ?? !!this.config.schema,
1378
+ };
1379
+ if (opts?.ip) {
1380
+ body['ip-override'] = opts.ip;
1381
+ }
1382
+ if (opts?.origin) {
1383
+ body['origin-override'] = opts.origin;
1384
+ }
1385
+
1386
+ const response = await jsonFetch(
1387
+ `${this.config.apiURI}/admin/query_perms_check?app_id=${this.config.appId}`,
1388
+ {
1389
+ method: 'POST',
1390
+ headers: authorizedHeaders(this.config, this.impersonationOpts),
1391
+ body: JSON.stringify(body),
1392
+ },
1393
+ );
1394
+
1395
+ return {
1396
+ result: response.result,
1397
+ checkResults: response['check-results'],
1398
+ };
1399
+ };
1400
+
1401
+ /**
1402
+ * Like `transact`, but does not write to the database.
1403
+ * Returns debugging information for permissions checks.
1404
+ * Useful for inspecting the values returned by the permissions checks.
1405
+ *
1406
+ * Requires a user/guest context to be set with `asUser`,
1407
+ * since permissions checks are user-specific.
1408
+ *
1409
+ * Accepts an optional configuration object with a `rules` key.
1410
+ * The provided rules will override the rules in the database for the duration of the transaction.
1411
+ *
1412
+ * @example
1413
+ * const goalId = id();
1414
+ * db.asUser({ guest: true }).debugTransact(
1415
+ * [db.tx.goals[goalId].update({title: "Get fit"})],
1416
+ * { rules: { goals: { allow: { update: "auth.id != null" } } }
1417
+ * )
1418
+ */
1419
+ debugTransact = (
1420
+ inputChunks: TransactionChunk<any, any> | TransactionChunk<any, any>[],
1421
+ opts?: {
1422
+ rules?: any;
1423
+ ip?: string | null | undefined;
1424
+ origin?: string | null | undefined;
1425
+ },
1426
+ ): Promise<DebugTransactResult> => {
1427
+ const body: any = {
1428
+ steps: steps(inputChunks),
1429
+ 'rules-override': opts?.rules,
1430
+ // @ts-expect-error because we're using a private API (for now)
1431
+ 'dangerously-commit-tx': opts?.__dangerouslyCommit,
1432
+ };
1433
+
1434
+ if (opts?.ip) {
1435
+ body['ip-override'] = opts.ip;
1436
+ }
1437
+ if (opts?.origin) {
1438
+ body['origin-override'] = opts.origin;
1439
+ }
1440
+
1441
+ return jsonFetch(
1442
+ `${this.config.apiURI}/admin/transact_perms_check?app_id=${this.config.appId}`,
1443
+ {
1444
+ method: 'POST',
1445
+ headers: authorizedHeaders(this.config, this.impersonationOpts),
1446
+ body: JSON.stringify(body),
1447
+ },
1448
+ );
1449
+ };
1450
+
1451
+ #setupSSEConnection() {
1452
+ if (this.#sseConnection) {
1453
+ this.#sseConnection.close();
1454
+ }
1455
+ const headers: HeadersInit = {
1456
+ ...authorizedHeaders(this.config, this.impersonationOpts),
1457
+ };
1458
+
1459
+ const inference = !!this.config.schema;
1460
+
1461
+ const ES = makeEventSourceWrapper({ headers, inference });
1462
+ const conn = new SSEConnection(
1463
+ ES,
1464
+ `${this.config.apiURI}/admin/sse?app_id=${this.config.appId}`,
1465
+ `${this.config.apiURI}/admin/sse/push?app_id=${this.config.appId}`,
1466
+ );
1467
+ conn.onopen = this.#onopen;
1468
+ conn.onmessage = this.#onmessage;
1469
+ conn.onclose = this.#onclose;
1470
+ conn.onerror = this.#onerror;
1471
+ this.#sseConnection = conn;
1472
+ return conn;
1473
+ }
1474
+
1475
+ #ensureSSEConnection() {
1476
+ return this.#sseConnection || this.#setupSSEConnection();
1477
+ }
1478
+
1479
+ #trySend(eventId, msg) {
1480
+ const sseConnection = this.#ensureSSEConnection();
1481
+ this.#log.info('[send]', eventId, msg, {
1482
+ isOpen: sseConnection.isOpen(),
1483
+ });
1484
+ if (sseConnection.isOpen()) {
1485
+ sseConnection.send({ 'client-event-id': eventId, ...msg });
1486
+ }
1487
+ }
1488
+
1489
+ #setupInstantStream() {
1490
+ this.#ensureSSEConnection();
1491
+ const instantStream = new InstantStream({
1492
+ WStream: this.config.WritableStream || WritableStream,
1493
+ RStream: this.config.ReadableStream || ReadableStream,
1494
+ trySend: (eventId, msg) => {
1495
+ this.#trySend(eventId, msg);
1496
+ },
1497
+ log: this.#log,
1498
+ });
1499
+
1500
+ this.#instantStream = instantStream;
1501
+ return instantStream;
1502
+ }
1503
+
1504
+ #ensureInstantStream() {
1505
+ return this.#instantStream || this.#setupInstantStream();
1506
+ }
1507
+
1508
+ #onopen = (e) => {
1509
+ if (e.target !== this.#sseConnection) {
1510
+ this.#log.info(
1511
+ '[socket][open]',
1512
+ e.target.id,
1513
+ 'skip; this is no longer the current transport',
1514
+ );
1515
+ return;
1516
+ }
1517
+ this.#log.info('[socket][open]', e.target.id);
1518
+ this.#sseBackoff = 0;
1519
+ this.#instantStream?.onConnectionStatusChange('authenticated');
1520
+ };
1521
+
1522
+ #onclose = (e) => {
1523
+ if (e.target !== this.#sseConnection) {
1524
+ this.#log.info(
1525
+ '[socket][close]',
1526
+ e.target.id,
1527
+ 'skip; this is no longer the current transport',
1528
+ );
1529
+ return;
1530
+ }
1531
+ this.#log.info('[socket][close]', e.target.id);
1532
+ this.#instantStream?.onConnectionStatusChange('closed');
1533
+ if (this.#sseConnection) {
1534
+ this.#sseConnection = null;
1535
+ if (!this.#connectionIsIdle()) {
1536
+ // We didn't remove the sse connection, and we have streams we care about, so let's try again
1537
+ setTimeout(() => this.#ensureSSEConnection(), this.#sseBackoff);
1538
+ this.#sseBackoff = Math.min(15000, Math.max(this.#sseBackoff, 500) * 2);
1539
+ }
1540
+ }
1541
+ };
1542
+
1543
+ #onerror = (e) => {
1544
+ if (e.target !== this.#sseConnection) {
1545
+ this.#log.info(
1546
+ '[socket][error]',
1547
+ e.target.id,
1548
+ 'skip; this is no longer the current transport',
1549
+ );
1550
+ return;
1551
+ }
1552
+ this.#log.info('[socket][error]', e.target.id);
1553
+
1554
+ this.#instantStream?.onConnectionStatusChange('closed');
1555
+ };
1556
+
1557
+ #connectionIsIdle() {
1558
+ return !this.#instantStream || !this.#instantStream.hasActiveStreams();
1559
+ }
1560
+
1561
+ #maybeShutdownConnection() {
1562
+ if (this.#sseConnection && this.#connectionIsIdle()) {
1563
+ const conn = this.#sseConnection;
1564
+ this.#log.info('cleaning up unused socket', conn.id);
1565
+ this.#sseConnection = null;
1566
+ conn.close();
1567
+ }
1568
+ }
1569
+
1570
+ #onmessage = (e) => {
1571
+ if (e.target !== this.#sseConnection) {
1572
+ this.#log.info(
1573
+ '[socket][message]',
1574
+ e.target.id,
1575
+ 'skip; this is no longer the current transport',
1576
+ );
1577
+ return;
1578
+ }
1579
+
1580
+ const msg = e.message;
1581
+ this.#log.info('[receive]', msg);
1582
+ switch (msg.op) {
1583
+ case 'start-stream-ok': {
1584
+ this.#instantStream?.onStartStreamOk(msg);
1585
+ break;
1586
+ }
1587
+ case 'stream-flushed': {
1588
+ this.#instantStream?.onStreamFlushed(msg);
1589
+ break;
1590
+ }
1591
+ case 'append-failed': {
1592
+ this.#instantStream?.onAppendFailed(msg);
1593
+ break;
1594
+ }
1595
+ case 'stream-append': {
1596
+ this.#instantStream?.onStreamAppend(msg);
1597
+ break;
1598
+ }
1599
+
1600
+ case 'error': {
1601
+ switch (msg['original-event']?.op) {
1602
+ case 'start-stream':
1603
+ case 'append-stream':
1604
+ case 'subscribe-stream':
1605
+ case 'unsubscribe-stream': {
1606
+ this.#instantStream?.onRecieveError(msg);
1607
+ break;
1608
+ }
1609
+ }
1610
+ break;
1611
+ }
1612
+ }
1613
+
1614
+ // Closes the connection if we don't have any items pending
1615
+ this.#maybeShutdownConnection();
1616
+ };
1617
+ }
1618
+
1619
+ export {
1620
+ init,
1621
+ init_experimental,
1622
+ id,
1623
+ tx,
1624
+ lookup,
1625
+ i,
1626
+ createInstantRouteHandler,
1627
+ type InstantRouteHandlerPayloadByType,
1628
+ type InstantRouteHandlerType,
1629
+ type InstantRouteHandlerBody,
1630
+ Webhooks,
1631
+ WebhooksManager,
1632
+ type WebhookAction,
1633
+ type WebhookStatus,
1634
+ type WebhookEventStatus,
1635
+ type WebhookInfo,
1636
+ type WebhookAttempt,
1637
+ type WebhookEventInfo,
1638
+ type WebhookEventsPage,
1639
+ type WebhookBody,
1640
+ type WebhookEntity,
1641
+ type WebhookPayload,
1642
+ type WebhookPayloadRecord,
1643
+ type WebhookPayloadRecordFor,
1644
+ type WebhookHandlerFn,
1645
+ type WebhookHandlers,
1646
+ type WebhookHelpers,
1647
+ type CreateWebhookParams,
1648
+ type UpdateWebhookParams,
1649
+
1650
+ // error
1651
+ InstantAPIError,
1652
+
1653
+ // warnings
1654
+ setInstantWarningsEnabled,
1655
+
1656
+ // types
1657
+ type Config,
1658
+ type ImpersonationOpts,
1659
+ type TransactionChunk,
1660
+ type DebugCheckResult,
1661
+ type InstantAdminDatabase,
1662
+
1663
+ // core types
1664
+ type User,
1665
+ type InstaQLParams,
1666
+ type ValidQuery,
1667
+ type Query,
1668
+ InstantError,
1669
+
1670
+ // query types
1671
+ type QueryResponse,
1672
+ type InstaQLResponse,
1673
+ type InstantQuery,
1674
+ type InstantUnknownSchemaDef,
1675
+ type InstantQueryResult,
1676
+ type InstantSchema,
1677
+ type InstantSchemaDatabase,
1678
+ type IInstantDatabase,
1679
+ type InstantObject,
1680
+ type InstantEntity,
1681
+ type BackwardsCompatibleSchema,
1682
+ type InstaQLFields,
1683
+ type SubscribeQueryCallback,
1684
+ type SubscribeQueryResponse,
1685
+ type SubscribeQueryPayload,
1686
+ type SubscriptionReadyState,
1687
+
1688
+ // schema types
1689
+ type AttrsDefs,
1690
+ type CardinalityKind,
1691
+ type DataAttrDef,
1692
+ type EntitiesDef,
1693
+ type EntitiesWithLinks,
1694
+ type EntityDef,
1695
+ type InstantGraph,
1696
+ type LinkAttrDef,
1697
+ type LinkDef,
1698
+ type LinksDef,
1699
+ type ResolveAttrs,
1700
+ type RoomsOf,
1701
+ type PresenceOf,
1702
+ type RoomsDef,
1703
+ type RoomHandle,
1704
+ type ValueTypes,
1705
+ type InstantSchemaDef,
1706
+ type InstantUnknownSchema,
1707
+ type InstaQLEntity,
1708
+ type InstaQLResult,
1709
+ type InstaQLEntitySubquery,
1710
+ type InstantRules,
1711
+ type CreateParams,
1712
+ type UpdateParams,
1713
+ type LinkParams,
1714
+
1715
+ // storage types
1716
+ type FileOpts,
1717
+ type UploadFileResponse,
1718
+ type DeleteFileResponse,
1719
+ type DeleteManyFileResponse,
1720
+
1721
+ // stream types
1722
+ type CreateReadStreamOpts,
1723
+ type CreateWriteStreamOpts,
1724
+ type InstantWritableStream,
1725
+
1726
+ // error types
1727
+ type InstantIssue,
1728
+
1729
+ // logger
1730
+ type Logger,
1731
+ };