@iebh/tera-fy 2.3.8 → 2.3.9

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,20 +44,22 @@ 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: {
56
+ HYPERDRIVE: PostgresSql;
47
57
  supabasey: BoundSupabaseyFunction;
48
58
  id: string; // Primary ID for the entity
49
59
  relation?: string; // Optional relation identifier (for namespaces, libraries)
50
60
  }) => Promise<any>;
51
-
52
61
  flushState: (args: {
62
+ HYPERDRIVE?: PostgresSql;
53
63
  supabasey: BoundSupabaseyFunction;
54
64
  state: any; // The state object to flush
55
65
  id?: string; // Primary ID (used in some lookups like namespaces)
@@ -58,49 +68,56 @@ interface SyncroEntityConfig {
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
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
70
82
  * @property {Function} flushState Function called to flush state from Firebase to Supabase. 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 institute = await HYPERDRIVE`
89
+ SELECT data
90
+ FROM institutes
91
+ WHERE id = ${id}
92
+ LIMIT 1
93
+ `;
94
+ if (institute.length > 0) {
95
+ return institute[0].data; // institute is valid and already exists
96
+ } else {
97
+ throw new Error(`Syncro institute "${id}" not found`);
98
+ }
83
99
  },
84
100
  flushState({supabasey, state, id}) {
85
- // @ts-expect-error Typescript struggles to resolve supabasey import correctly
86
- return supabasey.rpc('syncro_merge_data', {
101
+ // FIXME: Better to reuse `env.HYPERDRIVE` instead of supabasey here in future
102
+ return supabasey((supabase) => supabase.rpc('syncro_merge_data', {
87
103
  table_name: 'institutes',
88
104
  entity_id: id,
89
105
  new_data: state,
90
- });
106
+ }));
91
107
  },
92
108
  }, // }}}
93
109
  projects: { // {{{
94
110
  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;
111
+ async initState({HYPERDRIVE, supabasey, id}: {HYPERDRIVE: PostgresSql, supabasey: BoundSupabaseyFunction, id: string}) {
112
+ let projects = await HYPERDRIVE`
113
+ SELECT data
114
+ FROM projects
115
+ WHERE id = ${id}
116
+ LIMIT 1
117
+ `;
118
+ if (projects.length == 0) throw new Error(`Syncro project "${id}" not found`);
119
+
120
+ const data = projects[0].data;
104
121
 
105
122
  // MIGRATION - Move data.temp{} into Supabase files + add pointer to filename {{{
106
123
  if (
@@ -113,7 +130,7 @@ const syncroConfig: SyncroConfig = {
113
130
  await Promise.all(
114
131
  Object.entries(data.temp)
115
132
  .filter(([, branch]) => typeof branch == 'object')
116
- .map(([toolKey, ]) => {
133
+ .map(([toolKey]) => {
117
134
  console.log(`[MIGRATION] Converting data.temp[${toolKey}]...`);
118
135
 
119
136
  const toolName = toolKey.split('-')[0];
@@ -161,8 +178,8 @@ const syncroConfig: SyncroConfig = {
161
178
  return data;
162
179
  },
163
180
  flushState({supabasey, state, fsId}) {
164
- // Import Supabasey because 'supabasey' lowercase is just a function
165
- return supabasey(supabase => supabase.rpc('syncro_merge_data', {
181
+ // FIXME: Better to reuse `env.HYPERDRIVE` instead of supabasey here in future
182
+ return supabasey((supabase) => supabase.rpc('syncro_merge_data', {
166
183
  table_name: 'projects',
167
184
  entity_id: fsId,
168
185
  new_data: state,
@@ -171,7 +188,7 @@ const syncroConfig: SyncroConfig = {
171
188
  }, // }}}
172
189
  project_libraries: { // {{{
173
190
  singular: 'project library',
174
- async initState({supabasey, id, relation}) {
191
+ async initState({id, relation, supabasey}: {HYPERDRIVE: PostgresSql, id: string, relation?: string, supabasey: BoundSupabaseyFunction}) {
175
192
  if (!relation || !/_\*$/.test(relation)) throw new Error('Project library relation missing, path should resemble "project_library::${PROJECT}::${LIBRARY_FILE_ID}_*"');
176
193
 
177
194
  const fileId = relation.replace(/_\*$/, '');
@@ -209,63 +226,33 @@ const syncroConfig: SyncroConfig = {
209
226
  throw new Error('Flushing project_libraries::* namespace is not yet supported');
210
227
  },
211
228
  }, // }}}
212
- project_namespaces: { // {{{
229
+ project_namespaces: { // NOT YET SUPPORTED {{{
213
230
  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
- }
231
+ async initState() {
232
+ throw new Error('Updating project_namespaces is not yet supported');
241
233
  },
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
- );
234
+ async flushState() {
235
+ throw new Error('Updating project_namespaces is not yet supported');
252
236
  },
253
237
  }, // }}}
254
238
  test: { // {{{
255
239
  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;
240
+ async initState({HYPERDRIVE, id}: {HYPERDRIVE: PostgresSql, id: string}) {
241
+ let rows = await HYPERDRIVE`
242
+ SELECT data
243
+ FROM test
244
+ WHERE id = ${id}
245
+ LIMIT 1
246
+ `;
247
+ if (rows.length > 0) {
248
+ return rows[0].data; // User is valid and already exists
249
+ } else {
250
+ throw new Error(`Syncro test "${id}" not found`);
251
+ }
266
252
  },
267
253
  flushState({supabasey, state, fsId}) {
268
- return supabasey(supabase => supabase.rpc('syncro_merge_data', {
254
+ // FIXME: Better to reuse `env.HYPERDRIVE` instead of supabasey here in future
255
+ return supabasey((supabase) => supabase.rpc('syncro_merge_data', {
269
256
  table_name: 'test',
270
257
  entity_id: fsId,
271
258
  new_data: state,
@@ -274,34 +261,38 @@ const syncroConfig: SyncroConfig = {
274
261
  }, // }}}
275
262
  users: { // {{{
276
263
  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
264
+ async initState({HYPERDRIVE, id}: {HYPERDRIVE: PostgresSql, id: string}) {
265
+ let user = await HYPERDRIVE`
266
+ SELECT data
267
+ FROM users
268
+ WHERE id = ${id}
269
+ LIMIT 1
270
+ `;
271
+ if (user.length > 0) return user[0].data; // User is valid and already exists
285
272
 
286
- // User row doesn't already exist - need to create stub
287
- const newUser = await supabasey((supabase) => supabase
288
- .from('users')
289
- .insert<UserRow>({
273
+ // User row doesn't already exist - this shouldn't happen if the user has correctly gone through the onboarding process
274
+ // but... *shrugs*, who knows
275
+ let newUser = await HYPERDRIVE`
276
+ INSERT INTO users
277
+ (
290
278
  id,
291
- data: {
279
+ data
280
+ )
281
+ VALUES (
282
+ ${id},
283
+ ${HYPERDRIVE.json({
292
284
  id,
293
285
  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.
286
+ })}::JSONB
287
+ )
288
+ `;
289
+ if (!newUser?.length) throw new Error(`Failed to create new user "${id}"`);
290
+ return newUser[0].data; // Return back the data that eventually got created - allowing for database triggers, default field values etc.
301
291
 
302
292
  },
303
293
  flushState({supabasey, state, fsId}) {
304
- return supabasey(supabase => supabase.rpc('syncro_merge_data', {
294
+ // FIXME: Better to reuse `env.HYPERDRIVE` instead of supabasey here in future
295
+ return supabasey((supabase) => supabase.rpc('syncro_merge_data', {
305
296
  table_name: 'users',
306
297
  entity_id: fsId,
307
298
  new_data: state,
@@ -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.9",
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=.",