@iebh/tera-fy 2.0.7 → 2.0.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,5 +1,6 @@
1
1
  import Reflib from '@iebh/reflib';
2
2
  import {v4 as uuid4} from 'uuid';
3
+ import Syncro from './syncro.js';
3
4
 
4
5
  /**
5
6
  * Entities we support Syncro paths for, each should correspond directly with a Firebase/Firestore collection name
@@ -10,13 +11,10 @@ import {v4 as uuid4} from 'uuid';
10
11
  * @property {Function} initState Function called to initialize state when Firestore has no existing document. Called as `({supabase:SupabaseClient, entity:String, id:String, relation?:string})` and expected to return the initial data object state
11
12
  * @property {Function} flushState Function called to flush state from Firebase to Supabase. Called the same as `initState` + `{state:Object}`
12
13
  */
13
- export default syncEntities = {
14
+ export default {
14
15
  projects: { // {{{
15
16
  singular: 'project',
16
- async initState({supabase, id, tera}) {
17
- debugger; // FIXME: Check that `tera` gets populated
18
- if (!tera) throw new Error('initState() for projects requires `{tera:app.service("$tera")}`');
19
-
17
+ async initState({supabase, id}) {
20
18
  const result = await Syncro.wrapSupabase(supabase.from('projects')
21
19
  .select('data')
22
20
  .limit(1)
@@ -31,36 +29,32 @@ export default syncEntities = {
31
29
  console.log('[MIGRATION] State of temp at start:', data.temp);
32
30
 
33
31
  // Check if temp variable exists
34
- if (!data.temp) return data;
35
-
36
- let shownAlert = false;
37
- for (const toolKey in data.temp) {
38
- // Check if temp has already been set to file path
39
- if (typeof data.temp[toolKey] !== 'object') {
40
- console.log(`[MIGRATION] Skipping conversion of ${toolKey}, not an object:`, data.temp[toolKey])
41
- return;
42
- }
32
+ if (
33
+ !data.temp // Project contains no temp subkey
34
+ || Object.values(data.temp).every(t => typeof t != 'object') // None of the temp keys are objects
35
+ ) return data;
43
36
 
44
- if (!shownAlert) {
45
- shownAlert = true;
46
- console.log('[MIGRATION] showing alert');
47
- alert('Data found that will be migrated to new TERA file storage system. This may take a few minutes, please do not close your browser or refresh.');
48
- }
37
+ throw new Error('Project data version unsupported');
49
38
 
50
- // Create filename based on existing key
51
- const toolName = toolKey.split('-')[0];
52
- const fileName = `data-${toolName}-${nanoid()}.json`;
53
- console.log('[MIGRATION] Creating filename:', fileName);
39
+ // MIGRATION - Move data.temp{} into Supabase files + add pointer to filename
40
+ await Promise.all(
41
+ Object.entries(data.temp)
42
+ .filter(([toolKey, branch]) => typeof branch == 'object')
43
+ .map(([toolKey, branch]) => {
44
+ console.log(`[MIGRATION] Converting data.temp[${toolKey}]...`);
54
45
 
55
- // Create file, set contents and overwrite key
56
- const projectFile = await tera.createProjectFile(fileName);
57
- console.log('[MIGRATION] Setting file contents:', projectFile, 'to:', data.temp[toolKey]);
58
- await projectFile.setContents(data.temp[toolKey])
59
- console.log('[MIGRATION] Overwriting temp key with filepath:', fileName);
60
- data.temp[toolKey] = fileName;
61
- }
46
+ const toolName = toolKey.split('-')[0];
47
+ const fileName = `data-${toolName}-${nanoid()}.json`;
48
+ console.log('[MIGRATION] Creating filename:', fileName);
62
49
 
63
- console.log("[MIGRATION] State of temp at end:", data.temp)
50
+ // FIXME: All of this needs converting over to a Cloudflare Worker compatible environment
51
+ /* const projectFile = await tera.createProjectFile(fileName);
52
+ console.log('[MIGRATION] Setting file contents:', projectFile, 'to:', data.temp[toolKey]);
53
+ await projectFile.setContents(data.temp[toolKey])
54
+ console.log('[MIGRATION] Overwriting temp key with filepath:', fileName);
55
+ data.temp[toolKey] = fileName; */
56
+ })
57
+ );
64
58
 
65
59
  return data;
66
60
  },
@@ -1,6 +1,7 @@
1
1
  import {chunk} from 'lodash-es';
2
2
  import {doc as FirestoreDocRef, getDoc as FirestoreGetDoc} from 'firebase/firestore';
3
- import Syncro, {syncEntities} from './syncro.js';
3
+ import Syncro from './syncro.js';
4
+ import SyncroEntities from './entities.js';
4
5
 
5
6
  /**
6
7
  * @class SyncroKeyed
@@ -115,10 +116,10 @@ export default class SyncroKeyed extends Syncro {
115
116
  this.debug('Populate initial SyncroKeyed state');
116
117
 
117
118
  // Extract base data + add document and return new hook
118
- if (!syncEntities[entity]) throw new Error(`Unknown Sync entity "${entity}"`);
119
+ if (!SyncroEntities[entity]) throw new Error(`Unknown Sync entity "${entity}"`);
119
120
 
120
121
  // Go fetch the initial state object
121
- let state = await syncEntities[entity].initState({
122
+ let state = await SyncroEntities[entity].initState({
122
123
  supabase: Syncro.supabase,
123
124
  fsCollection, fsId,
124
125
  entity, id, relation,
@@ -57,6 +57,15 @@ export default class Syncro {
57
57
  static session;
58
58
 
59
59
 
60
+ /**
61
+ * OPTIONAL SyncroEntiries from './entiries.js' if its required
62
+ * This only gets populated if `config.forceLocalInit` is truthy and we've mounted at least one Syncro
63
+ *
64
+ * @type {Object}
65
+ */
66
+ static SyncroEntities;
67
+
68
+
60
69
  /**
61
70
  * This instances fully formed string path
62
71
  *
@@ -95,12 +104,12 @@ export default class Syncro {
95
104
  *
96
105
  * @type {Object}
97
106
  * @property {Number} heartbeatinterval Time in milliseconds between heartbeat beacons
98
- * @property {String} syncWorkerUrl The prefix Sync worker URL
107
+ * @property {String} syncroRegistryUrl The prefix Sync worker URL, used to populate Syncros and determine their active status
99
108
  * @property {Object} context Additional named parameters to pass to callbacks like initState
100
109
  */
101
110
  config = {
102
111
  heartbeatInterval: 50_000, //~= 50s
103
- syncWorkerUrl: 'https://tera-tools.com/api/sync',
112
+ syncroRegistryUrl: 'https://tera-tools.com/api/sync',
104
113
  context: {},
105
114
  };
106
115
 
@@ -245,7 +254,7 @@ export default class Syncro {
245
254
  let extracted = { ...pathMatcher.exec(path)?.groups };
246
255
 
247
256
  if (!extracted) throw new Error(`Invalid session path syntax "${path}"`);
248
- if (!(extracted.entity in syncEntities)) throw new Error(`Unsupported entity "${path}" -> Entity="${extracted.entity}"`);
257
+ if (Syncro.SyncroEntities && !(extracted.entity in SyncroEntities)) throw new Error(`Unsupported entity "${path}" -> Entity="${extracted.entity}"`);
249
258
 
250
259
  return {
251
260
  ...extracted,
@@ -300,6 +309,40 @@ export default class Syncro {
300
309
  }
301
310
 
302
311
 
312
+ /**
313
+ * Convert a raw POJO into Firestore field layout
314
+ * Field structures are usually consumed by the Firestore ReST API and need converting before being used
315
+ * NOTE: This does not serialize the incoming data so you likely want to use this as `toFirestoreFields(toFirestore(data))`
316
+ *
317
+ * @see https://stackoverflow.com/a/62304377
318
+ * @param {Object} data The raw value to convert
319
+ * @returns {Object} A Firestore compatible, typed data structure
320
+ */
321
+ static toFirestoreFields(data) {
322
+ const result = {};
323
+
324
+ for (const [key, value] of Object.entries(data)) {
325
+ const type = typeof value;
326
+
327
+ if (type === 'string') { // eslint-disable-line unicorn/prefer-switch
328
+ result[key] = { stringValue: value };
329
+ } else if (type === 'number') {
330
+ result[key] = { doubleValue: value };
331
+ } else if (type === 'boolean') {
332
+ result[key] = { booleanValue: value };
333
+ } else if (value === null) {
334
+ result[key] = { nullValue: null };
335
+ } else if (Array.isArray(value)) {
336
+ result[key] = { arrayValue: { values: value.map(item => Syncro.toFirestoreFields({ item }).item) } };
337
+ } else if (type === 'object') {
338
+ result[key] = { mapValue: { fields: Syncro.toFirestoreFields(value) } };
339
+ }
340
+ }
341
+
342
+ return result;
343
+ }
344
+
345
+
303
346
  /**
304
347
  * Convert a Firestore field dump into a native POJO
305
348
  * Field structures are usually provided by the Firestore ReST API and need de-typing back into a native document
@@ -467,18 +510,10 @@ export default class Syncro {
467
510
  this.debug('Populate initial Syncro state (from provided initialState)');
468
511
  return this.setFirestoreState(settings.initialState, {method: 'set'});
469
512
  } else {
470
- this.debug(`Populate initial Syncro state (from "${entity}" entity)`);
471
-
472
- // Extract base data + add document and return new hook
473
- return Promise.resolve()
474
- .then(()=> syncEntities[entity] || Promise.reject(`Unknown Sync entity "${entity}"`))
475
- .then(()=> syncEntities[entity].initState({
476
- supabase: Syncro.supabase,
477
- fsCollection, fsId,
478
- entity, id, relation,
479
- ...this.config.context,
480
- }))
481
- .then(state => this.setFirestoreState(state, {method: 'set'})) // Send new base state to Firestore
513
+ this.debug(`Populate initial Syncro state (from "${entity}" Syncro worker)`);
514
+
515
+ return fetch(`${this.config.syncroRegistryUrl}/${this.path}`)
516
+ .then(response => response.ok || Promise.reject(`Failed to check Syncro "${path}" status - ${response.statusText}`))
482
517
  }
483
518
  })
484
519
  .then(()=> { // Setup local state watcher
@@ -536,7 +571,7 @@ export default class Syncro {
536
571
  async heartbeat() {
537
572
  this.debug('heartbeat!');
538
573
 
539
- await fetch(`${this.config.syncWorkerUrl}/${this.path}/heartbeat`, {
574
+ await fetch(`${this.config.syncroRegistryUrl}/${this.path}/heartbeat`, {
540
575
  method: 'post',
541
576
  headers: {
542
577
  'Content-Type': 'application/json'
@@ -618,7 +653,7 @@ export default class Syncro {
618
653
  ...options,
619
654
  };
620
655
 
621
- return fetch(`${this.config.syncWorkerUrl}/${this.path}/flush` + (settings.destroy ? '?destroy=1' : ''))
656
+ return fetch(`${this.config.syncroRegistryUrl}/${this.path}/flush` + (settings.destroy ? '?destroy=1' : ''))
622
657
  .then(response => response.ok ? null : Promise.reject(response.statusText || 'An error occured'))
623
658
  }
624
659
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iebh/tera-fy",
3
- "version": "2.0.7",
3
+ "version": "2.0.9",
4
4
  "description": "TERA website worker",
5
5
  "scripts": {
6
6
  "dev": "esbuild --platform=browser --format=esm --bundle lib/terafy.client.js --outfile=dist/terafy.js --minify --serve --servedir=.",
@@ -33,7 +33,7 @@
33
33
  "./projectFile": "./lib/projectFile.js",
34
34
  "./proxy": "./lib/terafy.proxy.js",
35
35
  "./server": "./lib/terafy.server.js",
36
- "./syncro": "./lib/syncro.js",
36
+ "./syncro": "./lib/syncro/syncro.js",
37
37
  "./syncro/*": "./lib/syncro/*.js",
38
38
  "./plugins/*": "./plugins/*.js",
39
39
  "./widgets/*": "./widgets/*"
@@ -74,7 +74,6 @@
74
74
  "node": ">=18"
75
75
  },
76
76
  "dependencies": {
77
- "@iebh/reflib": "^2.5.4",
78
77
  "@momsfriendlydevco/marshal": "^2.1.4",
79
78
  "detect-port": "^2.1.0",
80
79
  "filesize": "^10.1.6",
@@ -95,6 +94,7 @@
95
94
  "nodemon": "^3.1.9"
96
95
  },
97
96
  "peerDependencies": {
97
+ "@iebh/reflib": "^2.5.4",
98
98
  "@supabase/supabase-js": "^2.48.1",
99
99
  "firebase": "^11.3.1",
100
100
  "vue": "^3.0.0"