@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.
- package/CHANGELOG.md +28 -0
- package/dist/plugin.vue2.es2019.js +12 -12
- package/lib/syncro/entities.js +25 -31
- package/lib/syncro/keyed.js +4 -3
- package/lib/syncro/syncro.js +52 -17
- package/package.json +3 -3
package/lib/syncro/entities.js
CHANGED
|
@@ -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
|
|
14
|
+
export default {
|
|
14
15
|
projects: { // {{{
|
|
15
16
|
singular: 'project',
|
|
16
|
-
async initState({supabase, id
|
|
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 (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
},
|
package/lib/syncro/keyed.js
CHANGED
|
@@ -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
|
|
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 (!
|
|
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
|
|
122
|
+
let state = await SyncroEntities[entity].initState({
|
|
122
123
|
supabase: Syncro.supabase,
|
|
123
124
|
fsCollection, fsId,
|
|
124
125
|
entity, id, relation,
|
package/lib/syncro/syncro.js
CHANGED
|
@@ -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}
|
|
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
|
-
|
|
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
|
|
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}"
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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"
|