@instantdb/core 0.22.87 → 0.22.88-experimental.drewh-ssr.20248709142.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 (49) hide show
  1. package/__tests__/src/serializeSchema.test.ts +123 -0
  2. package/dist/commonjs/Reactor.d.ts +13 -1
  3. package/dist/commonjs/Reactor.d.ts.map +1 -1
  4. package/dist/commonjs/Reactor.js +64 -5
  5. package/dist/commonjs/Reactor.js.map +1 -1
  6. package/dist/commonjs/createRouteHandler.d.ts +8 -0
  7. package/dist/commonjs/createRouteHandler.d.ts.map +1 -0
  8. package/dist/commonjs/createRouteHandler.js +57 -0
  9. package/dist/commonjs/createRouteHandler.js.map +1 -0
  10. package/dist/commonjs/framework.d.ts +77 -0
  11. package/dist/commonjs/framework.d.ts.map +1 -0
  12. package/dist/commonjs/framework.js +199 -0
  13. package/dist/commonjs/framework.js.map +1 -0
  14. package/dist/commonjs/index.d.ts +5 -1
  15. package/dist/commonjs/index.d.ts.map +1 -1
  16. package/dist/commonjs/index.js +7 -1
  17. package/dist/commonjs/index.js.map +1 -1
  18. package/dist/commonjs/parseSchemaFromJSON.d.ts +3 -0
  19. package/dist/commonjs/parseSchemaFromJSON.d.ts.map +1 -0
  20. package/dist/commonjs/parseSchemaFromJSON.js +148 -0
  21. package/dist/commonjs/parseSchemaFromJSON.js.map +1 -0
  22. package/dist/esm/Reactor.d.ts +13 -1
  23. package/dist/esm/Reactor.d.ts.map +1 -1
  24. package/dist/esm/Reactor.js +64 -5
  25. package/dist/esm/Reactor.js.map +1 -1
  26. package/dist/esm/createRouteHandler.d.ts +8 -0
  27. package/dist/esm/createRouteHandler.d.ts.map +1 -0
  28. package/dist/esm/createRouteHandler.js +53 -0
  29. package/dist/esm/createRouteHandler.js.map +1 -0
  30. package/dist/esm/framework.d.ts +77 -0
  31. package/dist/esm/framework.d.ts.map +1 -0
  32. package/dist/esm/framework.js +159 -0
  33. package/dist/esm/framework.js.map +1 -0
  34. package/dist/esm/index.d.ts +5 -1
  35. package/dist/esm/index.d.ts.map +1 -1
  36. package/dist/esm/index.js +5 -2
  37. package/dist/esm/index.js.map +1 -1
  38. package/dist/esm/parseSchemaFromJSON.d.ts +3 -0
  39. package/dist/esm/parseSchemaFromJSON.d.ts.map +1 -0
  40. package/dist/esm/parseSchemaFromJSON.js +144 -0
  41. package/dist/esm/parseSchemaFromJSON.js.map +1 -0
  42. package/dist/standalone/index.js +1648 -1348
  43. package/dist/standalone/index.umd.cjs +3 -3
  44. package/package.json +2 -2
  45. package/src/Reactor.js +74 -9
  46. package/src/createRouteHandler.ts +44 -0
  47. package/src/framework.ts +281 -0
  48. package/src/index.ts +9 -0
  49. package/src/parseSchemaFromJSON.ts +176 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instantdb/core",
3
- "version": "0.22.87",
3
+ "version": "0.22.88-experimental.drewh-ssr.20248709142.1",
4
4
  "description": "Instant's core local abstraction",
5
5
  "homepage": "https://github.com/instantdb/instant/tree/main/client/packages/core",
6
6
  "repository": {
@@ -53,7 +53,7 @@
53
53
  "dependencies": {
54
54
  "mutative": "^1.0.10",
55
55
  "uuid": "^11.1.0",
56
- "@instantdb/version": "0.22.87"
56
+ "@instantdb/version": "0.22.88-experimental.drewh-ssr.20248709142.1"
57
57
  },
58
58
  "scripts": {
59
59
  "test": "vitest",
package/src/Reactor.js CHANGED
@@ -319,7 +319,17 @@ export default class Reactor {
319
319
  this._oauthCallbackResponse = this._oauthLoginInit();
320
320
 
321
321
  // kick off a request to cache it
322
- this.getCurrentUser();
322
+ this.getCurrentUser().then((userInfo) => {
323
+ this.syncUserToEndpoint(userInfo.user);
324
+ });
325
+
326
+ setInterval(
327
+ async () => {
328
+ const currentUser = await this.getCurrentUser();
329
+ this.syncUserToEndpoint(currentUser.user);
330
+ },
331
+ 1000 * 60 * 20,
332
+ );
323
333
 
324
334
  NetworkListener.getIsOnline().then((isOnline) => {
325
335
  this._isOnline = isOnline;
@@ -513,6 +523,42 @@ export default class Reactor {
513
523
  }
514
524
  }
515
525
 
526
+ /**
527
+ * Does the same thing as add-query-ok
528
+ * but called as a result of receiving query info from ssr
529
+ * @param {any} q
530
+ * @param {{ triples: any; pageInfo: any; }} result
531
+ * @param {boolean} enableCardinalityInference
532
+ */
533
+ _addQueryData(q, result, enableCardinalityInference) {
534
+ if (!this.attrs) {
535
+ throw new Error('Attrs in reactor have not been set');
536
+ }
537
+ const queryHash = weakHash(q);
538
+ const store = s.createStore(
539
+ this.attrs,
540
+ result.triples,
541
+ enableCardinalityInference,
542
+ this._linkIndex,
543
+ this.config.useDateObjects,
544
+ );
545
+ this.querySubs.updateInPlace((prev) => {
546
+ prev[queryHash] = {
547
+ result: {
548
+ store,
549
+ pageInfo: result.pageInfo,
550
+ processedTxId: undefined,
551
+ isExternal: true,
552
+ },
553
+ q,
554
+ };
555
+ });
556
+ this._cleanupPendingMutationsQueries();
557
+ this.notifyOne(queryHash);
558
+ this.notifyOneQueryOnce(queryHash);
559
+ this._cleanupPendingMutationsTimeout();
560
+ }
561
+
516
562
  _handleReceive(connId, msg) {
517
563
  // opt-out, enabled by default if schema
518
564
  const enableCardinalityInference =
@@ -1146,7 +1192,7 @@ export default class Reactor {
1146
1192
  }
1147
1193
 
1148
1194
  /** Runs instaql on a query and a store */
1149
- dataForQuery(hash) {
1195
+ dataForQuery(hash, applyOptimistic = true) {
1150
1196
  const errorMessage = this._errorMessage;
1151
1197
  if (errorMessage) {
1152
1198
  return { error: errorMessage };
@@ -1170,17 +1216,16 @@ export default class Reactor {
1170
1216
  return cached;
1171
1217
  }
1172
1218
 
1173
- const { store, pageInfo, aggregate, processedTxId } = result;
1219
+ let store = result.store;
1220
+ const { pageInfo, aggregate, processedTxId } = result;
1174
1221
  const mutations = this._rewriteMutationsSorted(
1175
1222
  store.attrs,
1176
1223
  pendingMutations,
1177
1224
  );
1178
- const newStore = this._applyOptimisticUpdates(
1179
- store,
1180
- mutations,
1181
- processedTxId,
1182
- );
1183
- const resp = instaql({ store: newStore, pageInfo, aggregate }, q);
1225
+ if (applyOptimistic) {
1226
+ store = this._applyOptimisticUpdates(store, mutations, processedTxId);
1227
+ }
1228
+ const resp = instaql({ store: store, pageInfo, aggregate }, q);
1184
1229
 
1185
1230
  return { data: resp, querySubVersion, pendingMutationsVersion };
1186
1231
  }
@@ -1912,7 +1957,27 @@ export default class Reactor {
1912
1957
  }
1913
1958
  }
1914
1959
 
1960
+ async syncUserToEndpoint(user) {
1961
+ if (this.config.cookieEndpoint) {
1962
+ try {
1963
+ fetch(this.config.cookieEndpoint + '/sync-auth', {
1964
+ method: 'POST',
1965
+ body: JSON.stringify({
1966
+ user: user,
1967
+ }),
1968
+ headers: {
1969
+ 'Content-Type': 'application/json',
1970
+ },
1971
+ });
1972
+ } catch (error) {
1973
+ console.error('Error syncing user with external endpoint', error);
1974
+ }
1975
+ }
1976
+ }
1977
+
1915
1978
  updateUser(newUser) {
1979
+ this.syncUserToEndpoint(newUser);
1980
+
1916
1981
  const newV = { error: undefined, user: newUser };
1917
1982
  this._currentUserCached = { isLoading: false, ...newV };
1918
1983
  this._dataForQueryCache = {};
@@ -0,0 +1,44 @@
1
+ export const createInstantRouteHandler = (config: {
2
+ appId: string;
3
+ apiURI?: string;
4
+ }) => {
5
+ async function handleUserSync(req: Request) {
6
+ const body = await req.json();
7
+ if (body.user && body.user.refresh_token) {
8
+ return new Response('sync', {
9
+ headers: {
10
+ // 7 day expiry
11
+ 'Set-Cookie': `instant_user=${JSON.stringify(body.user)}; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=604800`,
12
+ },
13
+ });
14
+ } else {
15
+ return new Response('sync', {
16
+ headers: {
17
+ // remove the cookie (some browsers)
18
+ 'Set-Cookie': `instant_user=; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=-1`,
19
+ },
20
+ });
21
+ }
22
+ }
23
+
24
+ return {
25
+ GET: async (_req: Request) => {
26
+ return new Response('Method not allowed', {
27
+ status: 405,
28
+ statusText: 'Method Not Allowed',
29
+ });
30
+ },
31
+ POST: async (req: Request) => {
32
+ const url = new URL(req.url);
33
+ const pathname = url.pathname;
34
+ const route = pathname.split('/')[pathname.split('/').length - 1];
35
+ switch (route) {
36
+ case 'sync-auth':
37
+ return await handleUserSync(req);
38
+ }
39
+ return new Response('Route not found', {
40
+ status: 404,
41
+ });
42
+ },
43
+ };
44
+ };
@@ -0,0 +1,281 @@
1
+ import {
2
+ coerceQuery,
3
+ InstantCoreDatabase,
4
+ InstantDBAttr,
5
+ weakHash,
6
+ } from './index.ts';
7
+ import * as s from './store.js';
8
+ import instaql from './instaql.js';
9
+ import { RuleParams } from './schemaTypes.ts';
10
+
11
+ export const isServer = typeof window === 'undefined' || 'Deno' in globalThis;
12
+
13
+ export type FrameworkConfig = {
14
+ token?: string | null;
15
+ db: InstantCoreDatabase<any, any>;
16
+ };
17
+
18
+ type QueryPromise =
19
+ | {
20
+ type: 'http';
21
+ triples: any;
22
+ attrs: any;
23
+ queryHash: any;
24
+ query: any;
25
+ pageInfo?: any;
26
+ }
27
+ | {
28
+ type: 'session';
29
+ queryResult: any;
30
+ };
31
+
32
+ export class FrameworkClient {
33
+ private params: FrameworkConfig;
34
+ private db: InstantCoreDatabase<any, any>;
35
+ public resultMap: Map<
36
+ string,
37
+ {
38
+ status: 'pending' | 'success' | 'error';
39
+ type: 'http' | 'session';
40
+ promise?: Promise<QueryPromise> | null;
41
+ data?: any;
42
+ error?: any;
43
+ }
44
+ > = new Map();
45
+
46
+ private queryResolvedCallbacks: ((result: {
47
+ triples: any;
48
+ attrs: any;
49
+ queryHash: any;
50
+ query: any;
51
+ pageInfo?: any;
52
+ }) => void)[] = [];
53
+
54
+ constructor(params: FrameworkConfig) {
55
+ this.params = params;
56
+ this.db = params.db;
57
+ this.resultMap = new Map<
58
+ string,
59
+ {
60
+ type: 'http' | 'session';
61
+ status: 'pending' | 'success' | 'error';
62
+ promise?: Promise<QueryPromise>;
63
+ data?: any;
64
+ error?: any;
65
+ }
66
+ >();
67
+ }
68
+
69
+ public subscribe = (
70
+ callback: (result: {
71
+ triples: any;
72
+ attrs: any;
73
+ queryHash: string;
74
+ pageInfo?: any;
75
+ }) => void,
76
+ ) => {
77
+ this.queryResolvedCallbacks.push(callback);
78
+ };
79
+
80
+ // Runs on the client when ssr gets html script tags
81
+ public addQueryResult = (queryKey: string, value: any) => {
82
+ this.resultMap.set(queryKey, {
83
+ type: value.type,
84
+ status: 'success',
85
+ data: value,
86
+ promise: null,
87
+ error: null,
88
+ });
89
+ // send the result to the client
90
+ if (!isServer) {
91
+ // make sure the attrs are there to create stores
92
+ if (!this.db._reactor.attrs) {
93
+ this.db._reactor._setAttrs(value.attrs);
94
+ }
95
+ this.db._reactor._addQueryData(
96
+ value.query,
97
+ value,
98
+ !!this.db._reactor.config.schema,
99
+ );
100
+ }
101
+ };
102
+
103
+ public query = (
104
+ _query: any,
105
+ opts?: {
106
+ ruleParams: RuleParams;
107
+ },
108
+ ): {
109
+ type: 'http' | 'session';
110
+ status: 'pending' | 'success' | 'error';
111
+ promise?: Promise<QueryPromise>;
112
+ data?: any;
113
+ error?: any;
114
+ } => {
115
+ const { hash, query } = this.hashQuery(_query, opts);
116
+
117
+ if (this.db._reactor.status === 'authenticated') {
118
+ const promise = this.db.queryOnce(_query, opts);
119
+ let entry = {
120
+ status: 'pending' as 'pending' | 'success' | 'error',
121
+ type: 'session' as 'http' | 'session',
122
+ data: undefined as any,
123
+ error: undefined as any,
124
+ promise: promise as any,
125
+ };
126
+ promise.then((result) => {
127
+ entry.status = 'success';
128
+ entry.data = result;
129
+ entry.promise = null;
130
+ });
131
+ promise.catch((error) => {
132
+ entry.status = 'error';
133
+ entry.error = error;
134
+ entry.promise = null;
135
+ });
136
+ return entry as any;
137
+ }
138
+
139
+ const promise = this.getTriplesAndAttrsForQuery(query);
140
+ let entry = {
141
+ status: 'pending' as 'pending' | 'success' | 'error',
142
+ type: 'http' as 'http' | 'session',
143
+ data: undefined as any,
144
+ error: undefined as any,
145
+ promise: promise as any,
146
+ };
147
+
148
+ promise.then((result) => {
149
+ entry.status = 'success';
150
+ entry.data = result;
151
+ entry.promise = null;
152
+ });
153
+ promise.catch((error) => {
154
+ entry.status = 'error';
155
+ entry.error = error;
156
+ entry.promise = null;
157
+ });
158
+
159
+ promise.then((result) => {
160
+ this.queryResolvedCallbacks.forEach((callback) => {
161
+ callback({
162
+ queryHash: hash,
163
+ query: query,
164
+ attrs: result.attrs,
165
+ triples: result.triples,
166
+ pageInfo: result.pageInfo,
167
+ });
168
+ });
169
+ });
170
+
171
+ this.resultMap.set(hash, entry);
172
+ return entry;
173
+ };
174
+
175
+ public getExistingResultForQuery = (
176
+ _query: any,
177
+ opts?: {
178
+ ruleParams: RuleParams;
179
+ },
180
+ ) => {
181
+ const { hash } = this.hashQuery(_query, opts);
182
+ return this.resultMap.get(hash);
183
+ };
184
+
185
+ public completeIsomorphic = (
186
+ query: any,
187
+ triples: any[],
188
+ attrs: InstantDBAttr[],
189
+ pageInfo?: any,
190
+ ) => {
191
+ const attrMap = {};
192
+ attrs.forEach((attr) => {
193
+ attrMap[attr.id] = attr;
194
+ });
195
+
196
+ const enableCardinalityInference =
197
+ Boolean(this.db?._reactor?.config?.schema) &&
198
+ ('cardinalityInference' in this.db?._reactor?.config
199
+ ? Boolean(this.db?._reactor.config?.cardinalityInference)
200
+ : true);
201
+
202
+ const store = s.createStore(
203
+ attrMap,
204
+ triples,
205
+ enableCardinalityInference,
206
+ null,
207
+ this.params.db._reactor.config.useDateObjects || false,
208
+ );
209
+ const resp = instaql(
210
+ { store: store, pageInfo: pageInfo, aggregate: undefined },
211
+ query,
212
+ );
213
+ return resp;
214
+ };
215
+
216
+ public hashQuery = (
217
+ _query: any,
218
+ opts?: {
219
+ ruleParams: RuleParams;
220
+ },
221
+ ): { hash: string; query: any } => {
222
+ if (_query && opts && 'ruleParams' in opts) {
223
+ _query = { $$ruleParams: opts['ruleParams'], ..._query };
224
+ }
225
+ const query = _query ? coerceQuery(_query) : null;
226
+ return { hash: weakHash(query), query: query };
227
+ };
228
+
229
+ public getTriplesAndAttrsForQuery = async (
230
+ query: any,
231
+ ): Promise<{
232
+ triples: any[];
233
+ attrs: InstantDBAttr[];
234
+ query: any;
235
+ queryHash: string;
236
+ type: 'http';
237
+ pageInfo?: any;
238
+ }> => {
239
+ const response = await fetch(
240
+ `${this.db._reactor.config.apiURI}/runtime/query`,
241
+ {
242
+ method: 'POST',
243
+ headers: {
244
+ 'app-id': this.params.db._reactor.config.appId,
245
+ 'Content-Type': 'application/json',
246
+ Authorization: this.params.token
247
+ ? `Bearer ${this.params.token}`
248
+ : undefined,
249
+ } as Record<string, string>,
250
+ body: JSON.stringify({
251
+ query: query,
252
+ }),
253
+ },
254
+ );
255
+
256
+ if (!response.ok) {
257
+ throw new Error('Error getting triples from server');
258
+ }
259
+
260
+ const data = await response.json();
261
+
262
+ const attrs = data?.attrs;
263
+ if (!attrs) {
264
+ throw new Error('No attrs');
265
+ }
266
+
267
+ // TODO: make safer
268
+ const triples = data.result?.[0].data?.['datalog-result']?.['join-rows'][0];
269
+
270
+ const pageInfo = data.result?.[0]?.data?.['page-info'];
271
+
272
+ return {
273
+ attrs,
274
+ triples,
275
+ type: 'http',
276
+ queryHash: this.hashQuery(query).hash,
277
+ query,
278
+ pageInfo,
279
+ };
280
+ };
281
+ }
package/src/index.ts CHANGED
@@ -20,10 +20,13 @@ import {
20
20
  validateTransactions,
21
21
  TransactionValidationError,
22
22
  } from './transactionValidation.ts';
23
+
23
24
  import {
24
25
  StorageInterface,
25
26
  type StorageInterfaceStoreName,
26
27
  } from './utils/PersistedObject.ts';
28
+ import { createInstantRouteHandler } from './createRouteHandler.ts';
29
+ import { parseSchemaFromJSON } from './parseSchemaFromJSON.ts';
27
30
 
28
31
  import type {
29
32
  PresenceOpts,
@@ -103,6 +106,7 @@ import type {
103
106
  } from './schemaTypes.ts';
104
107
  import type { InstantRules } from './rulesTypes.ts';
105
108
  import type { UploadFileResponse, DeleteFileResponse } from './StorageAPI.ts';
109
+ import { FrameworkClient, type FrameworkConfig } from './framework.ts';
106
110
 
107
111
  import type {
108
112
  ExchangeCodeForTokenParams,
@@ -153,6 +157,7 @@ export type InstantConfig<
153
157
  appId: string;
154
158
  schema?: S;
155
159
  websocketURI?: string;
160
+ cookieEndpoint?: string;
156
161
  apiURI?: string;
157
162
  devtool?: boolean | DevtoolConfig;
158
163
  verbose?: boolean;
@@ -902,7 +907,9 @@ export {
902
907
  validateQuery,
903
908
  QueryValidationError,
904
909
  validateTransactions,
910
+ parseSchemaFromJSON,
905
911
  TransactionValidationError,
912
+ FrameworkClient,
906
913
 
907
914
  // error
908
915
  InstantAPIError,
@@ -1018,6 +1025,7 @@ export {
1018
1025
 
1019
1026
  // SSE
1020
1027
  type EventSourceType,
1028
+ type FrameworkConfig,
1021
1029
 
1022
1030
  // sync table types
1023
1031
  type SyncTableCallback,
@@ -1034,4 +1042,5 @@ export {
1034
1042
  // storage (e.g. indexeddb) interface
1035
1043
  StorageInterface,
1036
1044
  type StorageInterfaceStoreName,
1045
+ createInstantRouteHandler,
1037
1046
  };
@@ -0,0 +1,176 @@
1
+ import { i } from './schema.ts';
2
+ import { DataAttrDef, InstantSchemaDef } from './schemaTypes.ts';
3
+
4
+ export const parseSchemaFromJSON = (
5
+ s: any,
6
+ ): InstantSchemaDef<any, any, any> => {
7
+ // Parse entities
8
+ const entities: Record<string, any> = {};
9
+
10
+ for (const [entityName, entityInfo] of Object.entries(s.entities)) {
11
+ const entityDef = entityInfo as any;
12
+ const attrs: Record<string, any> = {};
13
+
14
+ // Parse attributes
15
+ for (const [attrName, attrInfo] of Object.entries(entityDef.attrs)) {
16
+ const attrDef = attrInfo as any;
17
+ let attr: DataAttrDef<any, any, any>;
18
+
19
+ // Create the appropriate attribute type
20
+ switch (attrDef.valueType) {
21
+ case 'string':
22
+ attr = i.string();
23
+ break;
24
+ case 'number':
25
+ attr = i.number();
26
+ break;
27
+ case 'boolean':
28
+ attr = i.boolean();
29
+ break;
30
+ case 'date':
31
+ attr = i.date();
32
+ break;
33
+ case 'json':
34
+ attr = i.json();
35
+ break;
36
+ default:
37
+ attr = i.json();
38
+ }
39
+
40
+ // Apply modifiers
41
+ if (!attrDef.required) {
42
+ attr = attr.optional();
43
+ }
44
+
45
+ if (attrDef.config?.indexed) {
46
+ attr = attr.indexed();
47
+ }
48
+
49
+ if (attrDef.config?.unique) {
50
+ attr = attr.unique();
51
+ }
52
+
53
+ attrs[attrName] = attr;
54
+ }
55
+
56
+ entities[entityName] = i.entity(attrs);
57
+ }
58
+
59
+ // Parse links
60
+ const links: Record<string, any> = s.links || {};
61
+
62
+ // Parse rooms
63
+ const rooms: Record<string, any> = {};
64
+
65
+ if (s.rooms) {
66
+ for (const [roomName, roomInfo] of Object.entries(s.rooms)) {
67
+ const roomDef = roomInfo as any;
68
+
69
+ // Parse presence
70
+ const presenceAttrs: Record<string, any> = {};
71
+ for (const [attrName, attrInfo] of Object.entries(
72
+ roomDef.presence.attrs,
73
+ )) {
74
+ const attrDef = attrInfo as any;
75
+ let attr: DataAttrDef<any, any, any>;
76
+
77
+ switch (attrDef.valueType) {
78
+ case 'string':
79
+ attr = i.string();
80
+ break;
81
+ case 'number':
82
+ attr = i.number();
83
+ break;
84
+ case 'boolean':
85
+ attr = i.boolean();
86
+ break;
87
+ case 'date':
88
+ attr = i.date();
89
+ break;
90
+ case 'json':
91
+ attr = i.json();
92
+ break;
93
+ default:
94
+ attr = i.json();
95
+ }
96
+
97
+ if (!attrDef.required) {
98
+ attr = attr.optional();
99
+ }
100
+
101
+ if (attrDef.config?.indexed) {
102
+ attr = attr.indexed();
103
+ }
104
+
105
+ if (attrDef.config?.unique) {
106
+ attr = attr.unique();
107
+ }
108
+
109
+ presenceAttrs[attrName] = attr;
110
+ }
111
+
112
+ // Parse topics
113
+ const topics: Record<string, any> = {};
114
+ if (roomDef.topics) {
115
+ for (const [topicName, topicInfo] of Object.entries(roomDef.topics)) {
116
+ const topicDef = topicInfo as any;
117
+ const topicAttrs: Record<string, any> = {};
118
+
119
+ for (const [attrName, attrInfo] of Object.entries(topicDef.attrs)) {
120
+ const attrDef = attrInfo as any;
121
+ let attr: DataAttrDef<any, any, any>;
122
+
123
+ switch (attrDef.valueType) {
124
+ case 'string':
125
+ attr = i.string();
126
+ break;
127
+ case 'number':
128
+ attr = i.number();
129
+ break;
130
+ case 'boolean':
131
+ attr = i.boolean();
132
+ break;
133
+ case 'date':
134
+ attr = i.date();
135
+ break;
136
+ case 'json':
137
+ attr = i.json();
138
+ break;
139
+ default:
140
+ attr = i.json();
141
+ }
142
+
143
+ if (!attrDef.required) {
144
+ attr = attr.optional();
145
+ }
146
+
147
+ if (attrDef.config?.indexed) {
148
+ attr = attr.indexed();
149
+ }
150
+
151
+ if (attrDef.config?.unique) {
152
+ attr = attr.unique();
153
+ }
154
+
155
+ topicAttrs[attrName] = attr;
156
+ }
157
+
158
+ topics[topicName] = i.entity(topicAttrs);
159
+ }
160
+ }
161
+
162
+ rooms[roomName] = {
163
+ presence: i.entity(presenceAttrs),
164
+ topics,
165
+ };
166
+ }
167
+ }
168
+
169
+ const resultingSchema = i.schema({
170
+ entities,
171
+ links,
172
+ rooms,
173
+ });
174
+
175
+ return resultingSchema;
176
+ };