@iebh/tera-fy 2.3.8 → 2.3.10

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.
@@ -1,21 +1,29 @@
1
- /* eslint-disable no-unused-vars */
1
+ /* eslint-disable jsdoc/reject-function-type, no-unused-vars */
2
2
  // @ts-expect-error TODO: Remove when reflib gets declaration file
3
3
  import Reflib from '@iebh/reflib';
4
4
  import {v4 as uuid4} from 'uuid';
5
5
  import {nanoid} from 'nanoid';
6
- import { BoundSupabaseyFunction } from '@iebh/supabasey';
6
+ import {BoundSupabaseyFunction} from '@iebh/supabasey';
7
+
8
+
9
+ // Minimal interface for a postgres-npm Sql instance (HYPERDRIVE is injected by the Cloudflare Worker runtime)
10
+ export interface PostgresSql {
11
+ (strings: TemplateStringsArray, ...values: any[]): Promise<any[]>;
12
+ json(value: any): any;
13
+ }
14
+
7
15
 
8
16
  // DATABASE TABLE TYPE DEFINITIONS {{{
9
17
  interface ProjectRow {
10
- data: {
11
- temp?: { [key: string]: any };
18
+ data: {
19
+ id: string;
12
20
  // TODO: add other properties in project data
13
21
  };
14
22
  // TODO: Define other columns in project table
15
23
  }
16
24
 
17
25
  interface InstituteRow {
18
- data: any;
26
+ data: any;
19
27
  }
20
28
 
21
29
  interface UserRow {
@@ -36,71 +44,81 @@ interface NamespaceRow {
36
44
  [key: string]: any;
37
45
  // TODO: add other properties in namespace data
38
46
  };
39
- // TODO: Define other namespace in user table
47
+ // TODO: Define other columns in namespace table
40
48
  }
41
49
  // }}}
42
50
 
43
- // Interface for each syncroconfic
51
+
52
+ // Interface for each syncro entity config
44
53
  interface SyncroEntityConfig {
45
54
  singular: string;
46
55
  initState: (args: {
47
- supabasey: BoundSupabaseyFunction;
48
- id: string; // Primary ID for the entity
49
- relation?: string; // Optional relation identifier (for namespaces, libraries)
56
+ HYPERDRIVE: PostgresSql;
57
+ supabasey: BoundSupabaseyFunction;
58
+ id: string; // Primary ID for the entity
59
+ relation?: string; // Optional relation identifier (for namespaces, libraries)
50
60
  }) => Promise<any>;
51
-
52
61
  flushState: (args: {
53
- supabasey: BoundSupabaseyFunction;
54
- state: any; // The state object to flush
55
- id?: string; // Primary ID (used in some lookups like namespaces)
56
- fsId?: string; // ID often passed to Supabase RPCs (might be same as 'id')
57
- relation?: string; // Optional relation identifier
62
+ HYPERDRIVE: PostgresSql;
63
+ supabasey?: BoundSupabaseyFunction;
64
+ state: any; // The state object to flush
65
+ id?: string; // Primary ID (used in some lookups like namespaces)
66
+ fsId?: string; // ID often passed to Supabase RPCs (might be same as 'id')
67
+ relation?: string; // Optional relation identifier
58
68
  }) => Promise<any>; // Return type signifies completion/result of flush
59
69
  }
60
70
 
71
+
61
72
  type SyncroConfig = Record<string, SyncroEntityConfig>;
62
73
 
74
+
63
75
  /**
64
76
  * Entities we support Syncro paths for, each should correspond directly with a Firebase/Firestore collection name
65
77
  *
66
78
  * @type {Object} An object lookup of entities
67
79
  *
68
80
  * @property {String} singular The singular noun for the item
69
- * @property {Function} initState Function called to initialize state when Firestore has no existing document. Called as `({supabase:BoundSupabaseyFunction, db:BoundHyperdriveInstance, entity:String, id:String, relation?:string})` and expected to return the initial data object state
70
- * @property {Function} flushState Function called to flush state from Firebase to Supabase. Called the same as `initState` + `{state:Object}`
81
+ * @property {Function} initState Function called to initialize state when Firestore has no existing document. Called as `({HYPERDRIVE:PostgresSql, supabasey:BoundSupabaseyFunction, id:String, relation?:string})` and expected to return the initial data object state
82
+ * @property {Function} flushState Function called to flush state from Firebase to Postgres. Called the same as `initState` + `{state:Object}`
71
83
  */
72
84
  const syncroConfig: SyncroConfig = {
73
85
  institutes: { // {{{
74
86
  singular: 'institute',
75
- async initState({supabasey, id}: {supabasey: BoundSupabaseyFunction, id: string}) {
76
- const institute = await supabasey((supabase) => supabase
77
- .from('institutes')
78
- .select('data')
79
- .eq('id', id)
80
- .maybeSingle<InstituteRow>()
81
- );
82
- if (institute) return institute.data; // institute is valid and already exists
87
+ async initState({HYPERDRIVE, id}: {HYPERDRIVE: PostgresSql, id: string}) {
88
+ let rows = await HYPERDRIVE`
89
+ SELECT data
90
+ FROM institutes
91
+ WHERE id = ${id}
92
+ LIMIT 1
93
+ `;
94
+ if (rows.length > 0) {
95
+ return rows[0].data; // institute is valid and already exists
96
+ } else {
97
+ throw new Error(`Syncro institute "${id}" not found`);
98
+ }
83
99
  },
84
- flushState({supabasey, state, id}) {
85
- // @ts-expect-error Typescript struggles to resolve supabasey import correctly
86
- return supabasey.rpc('syncro_merge_data', {
87
- table_name: 'institutes',
88
- entity_id: id,
89
- new_data: state,
90
- });
100
+ flushState({HYPERDRIVE, state, id}) {
101
+ return HYPERDRIVE`
102
+ SELECT syncro_merge_data(
103
+ table_name => 'institutes',
104
+ entity_id => ${id}::UUID,
105
+ new_data => ${HYPERDRIVE.json(state)}::JSONB
106
+ )
107
+ `;
91
108
  },
92
109
  }, // }}}
93
110
  projects: { // {{{
94
111
  singular: 'project',
95
- async initState({supabasey, id}) {
96
- const projectData = await supabasey((supabase) => supabase
97
- .from('projects')
98
- .select('data')
99
- .eq('id', id)
100
- .maybeSingle<ProjectRow>()
101
- );
102
- if (!projectData) throw new Error(`Syncro project "${id}" not found`);
103
- const data = projectData.data;
112
+ async initState({HYPERDRIVE, supabasey, id}: {HYPERDRIVE: PostgresSql, supabasey: BoundSupabaseyFunction, id: string}) {
113
+ let rows = await HYPERDRIVE`
114
+ SELECT data
115
+ FROM projects
116
+ WHERE id = ${id}
117
+ LIMIT 1
118
+ `;
119
+ if (rows.length == 0) throw new Error(`Syncro project "${id}" not found`);
120
+
121
+ const data = rows[0].data;
104
122
 
105
123
  // MIGRATION - Move data.temp{} into Supabase files + add pointer to filename {{{
106
124
  if (
@@ -113,7 +131,7 @@ const syncroConfig: SyncroConfig = {
113
131
  await Promise.all(
114
132
  Object.entries(data.temp)
115
133
  .filter(([, branch]) => typeof branch == 'object')
116
- .map(([toolKey, ]) => {
134
+ .map(([toolKey]) => {
117
135
  console.log(`[MIGRATION] Converting data.temp[${toolKey}]...`);
118
136
 
119
137
  const toolName = toolKey.split('-')[0];
@@ -160,18 +178,19 @@ const syncroConfig: SyncroConfig = {
160
178
 
161
179
  return data;
162
180
  },
163
- flushState({supabasey, state, fsId}) {
164
- // Import Supabasey because 'supabasey' lowercase is just a function
165
- return supabasey(supabase => supabase.rpc('syncro_merge_data', {
166
- table_name: 'projects',
167
- entity_id: fsId,
168
- new_data: state,
169
- }));
181
+ flushState({HYPERDRIVE, state, fsId}) {
182
+ return HYPERDRIVE`
183
+ SELECT syncro_merge_data(
184
+ table_name => 'projects',
185
+ entity_id => ${fsId}::UUID,
186
+ new_data => ${HYPERDRIVE.json(state)}::JSONB
187
+ )
188
+ `;
170
189
  },
171
190
  }, // }}}
172
191
  project_libraries: { // {{{
173
192
  singular: 'project library',
174
- async initState({supabasey, id, relation}) {
193
+ async initState({id, relation, supabasey}: {HYPERDRIVE: PostgresSql, id: string, relation?: string, supabasey: BoundSupabaseyFunction}) {
175
194
  if (!relation || !/_\*$/.test(relation)) throw new Error('Project library relation missing, path should resemble "project_library::${PROJECT}::${LIBRARY_FILE_ID}_*"');
176
195
 
177
196
  const fileId = relation.replace(/_\*$/, '');
@@ -209,103 +228,79 @@ const syncroConfig: SyncroConfig = {
209
228
  throw new Error('Flushing project_libraries::* namespace is not yet supported');
210
229
  },
211
230
  }, // }}}
212
- project_namespaces: { // {{{
231
+ project_namespaces: { // NOT YET SUPPORTED {{{
213
232
  singular: 'project namespace',
214
- async initState({supabasey, id, relation}) {
215
- if (!relation) throw new Error('Project namespace relation missing, path should resemble "project_namespaces::${PROJECT}::${RELATION}"');
216
- const rows = await supabasey((supabase) => supabase
217
- .from('project_namespaces')
218
- .select('data')
219
- .eq('project', id)
220
- .eq('name', relation)
221
- .limit(1)
222
- );
223
-
224
- if (rows && rows.length == 1) {
225
- return rows[0].data;
226
- } else {
227
- const newItem = await supabasey((supabase) => supabase
228
- .from('project_namespaces') // Doesn't exist - create it
229
- .insert<NamespaceRow>({
230
- project: id,
231
- name: relation,
232
- data: {},
233
- })
234
- .select('data')
235
- .single<NamespaceRow>() // Assuming insert returns the single inserted row
236
- );
237
-
238
- if (!newItem) throw new Error('Failed to create project namespace');
239
- return newItem.data;
240
- }
233
+ async initState() {
234
+ throw new Error('Updating project_namespaces is not yet supported');
241
235
  },
242
- flushState({supabasey, state, id, relation}) {
243
- return supabasey((supabase) => supabase
244
- .from('project_namespaces')
245
- .update({
246
- edited_at: new Date().toISOString(),
247
- data: state,
248
- })
249
- .eq('project', id)
250
- .eq('name', relation)
251
- );
236
+ async flushState() {
237
+ throw new Error('Updating project_namespaces is not yet supported');
252
238
  },
253
239
  }, // }}}
254
240
  test: { // {{{
255
241
  singular: 'test',
256
- async initState({supabasey, id}: {supabasey: BoundSupabaseyFunction, id: string}) {
257
- const rows = await supabasey((supabase) => supabase
258
- .from('test')
259
- .select('data')
260
- .eq('id', id)
261
- .limit(1)
262
- );
263
-
264
- if (!rows || rows.length !== 1) return Promise.reject(`Syncro test item "${id}" not found`);
265
- return rows[0].data;
242
+ async initState({HYPERDRIVE, id}: {HYPERDRIVE: PostgresSql, id: string}) {
243
+ let rows = await HYPERDRIVE`
244
+ SELECT data
245
+ FROM test
246
+ WHERE id = ${id}
247
+ LIMIT 1
248
+ `;
249
+ if (rows.length > 0) {
250
+ return rows[0].data; // User is valid and already exists
251
+ } else {
252
+ throw new Error(`Syncro test "${id}" not found`);
253
+ }
266
254
  },
267
- flushState({supabasey, state, fsId}) {
268
- return supabasey(supabase => supabase.rpc('syncro_merge_data', {
269
- table_name: 'test',
270
- entity_id: fsId,
271
- new_data: state,
272
- }));
255
+ flushState({HYPERDRIVE, state, fsId}) {
256
+ return HYPERDRIVE`
257
+ SELECT syncro_merge_data(
258
+ table_name => 'test',
259
+ entity_id => ${fsId}::UUID,
260
+ new_data => ${HYPERDRIVE.json(state)}::JSONB
261
+ )
262
+ `;
273
263
  },
274
264
  }, // }}}
275
265
  users: { // {{{
276
266
  singular: 'user',
277
- async initState({supabasey, id}: {supabasey: BoundSupabaseyFunction, id: string}) {
278
- const user = await supabasey((supabase) => supabase
279
- .from('users')
280
- .select('data')
281
- .eq('id', id)
282
- .maybeSingle<UserRow>()
283
- );
284
- if (user) return user.data; // User is valid and already exists
267
+ async initState({HYPERDRIVE, id}: {HYPERDRIVE: PostgresSql, id: string}) {
268
+ let rows = await HYPERDRIVE`
269
+ SELECT data
270
+ FROM users
271
+ WHERE id = ${id}
272
+ LIMIT 1
273
+ `;
274
+ if (rows.length > 0) return rows[0].data; // User is valid and already exists
285
275
 
286
- // User row doesn't already exist - need to create stub
287
- const newUser = await supabasey((supabase) => supabase
288
- .from('users')
289
- .insert<UserRow>({
276
+ // User row doesn't already exist - this shouldn't happen if the user has correctly gone through the onboarding process
277
+ // but... *shrugs*, who knows
278
+ let newUser = await HYPERDRIVE`
279
+ INSERT INTO users
280
+ (
290
281
  id,
291
- data: {
282
+ data
283
+ )
284
+ VALUES (
285
+ ${id},
286
+ ${HYPERDRIVE.json({
292
287
  id,
293
288
  credits: 1000,
294
- },
295
- })
296
- .select('data')
297
- .single<UserRow>() // Assuming insert returns the single inserted row
298
- );
299
- if (!newUser) throw new Error('Failed to create user');
300
- return newUser.data; // Return back the data that eventually got created - allowing for database triggers, default field values etc.
289
+ })}::JSONB
290
+ )
291
+ `;
292
+ if (!newUser?.length) throw new Error(`Failed to create new user "${id}"`);
293
+ return newUser[0].data; // Return back the data that eventually got created - allowing for database triggers, default field values etc.
301
294
 
302
295
  },
303
- flushState({supabasey, state, fsId}) {
304
- return supabasey(supabase => supabase.rpc('syncro_merge_data', {
305
- table_name: 'users',
306
- entity_id: fsId,
307
- new_data: state,
308
- }));
296
+ flushState({HYPERDRIVE, state, fsId}) {
297
+ return HYPERDRIVE`
298
+ SELECT syncro_merge_data(
299
+ table_name => 'users',
300
+ entity_id => ${fsId}::UUID,
301
+ new_data => ${HYPERDRIVE.json(state)}::JSONB
302
+ )
303
+ `;
309
304
  },
310
305
  }, // }}}
311
306
  };
@@ -137,6 +137,7 @@ export default class SyncroKeyed extends Syncro {
137
137
 
138
138
  // Go fetch the initial state object
139
139
  const state = await SyncroEntities[entityKey].initState({
140
+ HYPERDRIVE: Syncro.db,
140
141
  supabasey: Syncro.supabasey,
141
142
  id, relation,
142
143
  });
@@ -25,6 +25,7 @@ import PromiseThrottle from 'p-throttle';
25
25
  import PromiseRetry from 'p-retry';
26
26
  import {FirebaseApp, FirebaseError} from 'firebase/app';
27
27
  import { BoundSupabaseyFunction } from '@iebh/supabasey';
28
+ import type { PostgresSql } from './entities.js';
28
29
 
29
30
  interface ThrottleOptions<T = any> {
30
31
  limit: number,
@@ -81,6 +82,14 @@ export default class Syncro {
81
82
  static supabasey: BoundSupabaseyFunction;
82
83
 
83
84
 
85
+ /**
86
+ * Postgres SQL instance in use (injected by the Cloudflare Worker runtime via Hyperdrive)
87
+ *
88
+ * @type {PostgresSql}
89
+ */
90
+ static db: PostgresSql;
91
+
92
+
84
93
  /**
85
94
  * The current user session, should be unique for the user + browser tab
86
95
  * Used by the heartbeat system
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iebh/tera-fy",
3
- "version": "2.3.8",
3
+ "version": "2.3.10",
4
4
  "description": "TERA website worker",
5
5
  "scripts": {
6
6
  "dev": "esbuild --platform=browser --format=esm --bundle lib/terafy.client.ts --outfile=dist/terafy.js --minify --serve --servedir=.",