@iebh/tera-fy 1.15.9 → 2.0.0

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.
@@ -469,6 +469,16 @@ export default class TeraFyServer {
469
469
  }
470
470
 
471
471
 
472
+ /**
473
+ * Provide an object of credentials for 3rd party services like Firebase/Supabase
474
+ *
475
+ * @returns {Object} An object containing 3rd party service credentials
476
+ */
477
+ getCredentials() {
478
+ return app.service('$auth').credentials;
479
+ }
480
+
481
+
472
482
  /**
473
483
  * In embed mode only - create a popup window and try to auth via that
474
484
  *
@@ -736,6 +746,54 @@ export default class TeraFyServer {
736
746
 
737
747
  // }}}
738
748
 
749
+ // Project namespaces - getNamespace(), setNamespace(), listNamespaces() {{{
750
+ /**
751
+ * Get a one-off snapshot of a namespace without mounting it
752
+ * This can be used for simpler apps which don't have their own reactive / observer equivelent
753
+ *
754
+ * @param {String} name The alias of the namespace, this should be alphanumeric + hyphens + underscores
755
+ *
756
+ * @returns {Promise<Object>} A promise which resolves to the namespace POJO state
757
+ */
758
+ getNamespace(name) {
759
+ if (!/^[\w-]+$/.test(name)) throw new Error('Namespaces must be alphanumeric + hyphens + underscores');
760
+
761
+ return this.$syncro.getSnapshot(`project_namespaces::${this.$projects.active.id}::${name}`);
762
+ }
763
+
764
+
765
+ /**
766
+ * Set (or merge by default) a one-off snapshot over an existing namespace
767
+ * This can be used for simpler apps which don't have their own reactive / observer equivelent and just want to quickly set something
768
+ *
769
+ * @param {String} name The name of the namespace
770
+ * @param {Object} state The state to merge
771
+ * @param {Object} [options] Additional options to mutate behaviour
772
+ * @param {'merge'|'set'} [options.method='merge'] How to handle the state. 'merge' (merge a partial state over the existing namespace state), 'set' (completely overwrite the existing namespace)
773
+ *
774
+ * @returns {Promise<Object>} A promise which resolves to the namespace POJO state
775
+ */
776
+ setNamespace(name, state, options) {
777
+ if (!/^[\w--]+$/.test(name)) throw new Error('Namespaces must be alphanumeric + hyphens + underscores');
778
+ if (typeof state != 'object') throw new Error('State must be an object');
779
+
780
+ return this.$syncro.setSnapshot(`project_namespaces::${this.$projects.active.id}}::${name}`, state, {
781
+ method: options.method,
782
+ });
783
+ }
784
+
785
+
786
+ /**
787
+ * Return a list of namespaces available to the current project
788
+ *
789
+ * @returns {Promise<Array<Object>>} Collection of available namespaces for the current project
790
+ * @property {String} name The name of the namespace
791
+ */
792
+ listNamespaces() {
793
+ return app.service('$projects').listNamespaces();
794
+ }
795
+ // }}}
796
+
739
797
  // Project State - getProjectState(), setProjectState(), setProjectStateDefaults(), replaceProjectState(), applyProjectStatePatch() {{{
740
798
 
741
799
  /**
@@ -819,13 +877,11 @@ export default class TeraFyServer {
819
877
  * @param {String|Array<String>} [path] The sub-path within the project state to set, if unspecifed the entire target is used as a target and a save operation is forced
820
878
  * @param {*} value The value to set as the default structure
821
879
  * @param {Object} [options] Additional options to mutate behaviour, see setProjectState() for the full list of supported options
822
- * @param {Boolean} [options.flush=true] Force a re-read from the server after setting, prevents race conditions with large objects. Calls `setProjectStateFlush()` after applying defaults
823
880
  *
824
881
  * @returns {Promise<*>} A promise which resolves to the eventual input value after defaults have been applied
825
882
  */
826
883
  setProjectStateDefaults(path, value, options) {
827
884
  let settings = {
828
- flush: true,
829
885
  ...options,
830
886
  };
831
887
  if (!app.service('$projects').active) throw new Error('No active project');
@@ -841,7 +897,6 @@ export default class TeraFyServer {
841
897
  ...settings,
842
898
  },
843
899
  )
844
- .then(()=> settings.flush && this.setProjectStateFlush())
845
900
  .then(()=> pathTools.get(target, path));
846
901
  } else { // Called as (value) - Populate entire project layout
847
902
  pathTools.defaults(target, path);
@@ -850,26 +905,11 @@ export default class TeraFyServer {
850
905
  newState: cloneDeep(target),
851
906
  });
852
907
  return this.saveProjectState()
853
- .then(()=> settings.flush && this.setProjectStateFlush())
854
908
  .then(()=> target);
855
909
  }
856
910
  }
857
911
 
858
912
 
859
- /**
860
- * Force copying local changes to the server
861
- * This is only ever needed when saving large quantities of data that need to be immediately available
862
- *
863
- * @returns {Promise} A promise which resolves when the operation has completed
864
- */
865
- setProjectStateFlush() {
866
- this.debug('INFO', 1, 'Force project state flush!');
867
- if (!app.service('$projects').active) throw new Error('No active project');
868
- return app.service('$projects').active.$touchLocal()
869
- .then(()=> null)
870
- }
871
-
872
-
873
913
  /**
874
914
  * Force refetching the remote project state into local
875
915
  *
@@ -1644,9 +1684,9 @@ export default class TeraFyServer {
1644
1684
  *
1645
1685
  * @param {Object} [options] Additional options to mutate behaviour
1646
1686
  * @param {String} [options.body] Optional additional body text
1687
+ * @param {Boolean} [options.isHtml=false] If truthy, treat the body as HTML
1647
1688
  * @param {String} [options.value] Current or default value to display pre-filled
1648
1689
  * @param {String} [options.title='Input required'] The dialog title to display
1649
- * @param {Boolean} [options.bodyHtml=false] If truthy, treat the body as HTML
1650
1690
  * @param {String} [options.placeholder] Optional placeholder text
1651
1691
  * @param {Boolean} [options.required=true] Treat nullish or empty inputs as a cancel operation
1652
1692
  *
@@ -1655,7 +1695,7 @@ export default class TeraFyServer {
1655
1695
  uiPrompt(text, options) {
1656
1696
  let settings = {
1657
1697
  body: '',
1658
- bodyHtml: false,
1698
+ isHtml: false,
1659
1699
  title: 'Input required',
1660
1700
  value: '',
1661
1701
  placeholder: '',
@@ -1674,7 +1714,7 @@ export default class TeraFyServer {
1674
1714
  component: 'UiPrompt',
1675
1715
  componentProps: {
1676
1716
  body: settings.body,
1677
- bodyHtml: settings.bodyHtml,
1717
+ isHtml: settings.isHtml,
1678
1718
  placeholder: settings.placeholder,
1679
1719
  value: settings.value,
1680
1720
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iebh/tera-fy",
3
- "version": "1.15.9",
3
+ "version": "2.0.0",
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=.",
@@ -72,6 +72,7 @@
72
72
  "node": ">=18"
73
73
  },
74
74
  "dependencies": {
75
+ "@momsfriendlydevco/marshal": "^2.1.1",
75
76
  "detect-port": "^1.6.1",
76
77
  "filesize": "^10.1.4",
77
78
  "http-proxy": "^1.18.1",
@@ -91,6 +92,8 @@
91
92
  "nodemon": "^3.1.7"
92
93
  },
93
94
  "peerDependencies": {
95
+ "@supabase/supabase-js": "^2.48.1",
96
+ "firebase": "^11.3.1",
94
97
  "vue": "^3.0.0"
95
98
  },
96
99
  "optionalDependencies": {
@@ -0,0 +1,122 @@
1
+ import {initializeApp as Firebase} from 'firebase/app';
2
+ import {getFirestore as Firestore} from 'firebase/firestore';
3
+ import {createClient as Supabase} from '@supabase/supabase-js'
4
+ import Syncro from '../lib/syncro.js';
5
+ import TeraFyPluginBase from './base.js';
6
+
7
+ /**
8
+ * Plugin which adds Firebase / Firestore support for namespace mounts
9
+ *
10
+ * @class TeraFyPluginFirebase
11
+ */
12
+ export default class TeraFyPluginFirebase extends TeraFyPluginBase {
13
+
14
+ /**
15
+ * Lookup object of mounted Syncro objects by path
16
+ *
17
+ * @type {Object<Syncro>}
18
+ */
19
+ syncros = {};
20
+
21
+
22
+ /**
23
+ * @interface
24
+ * The Syncro#reactive option to use when creating new Syncro instances
25
+ * This is expected to be overriden by other plugins
26
+ * If falsy the Syncro module will fall back to its internal (POJO only) getReactive() function
27
+ *
28
+ * @name getReactive
29
+ * @type {Function} A reactive function as defined in Syncro
30
+ */
31
+
32
+
33
+ /**
34
+ * Setup Firebase + Firestore + Supabase
35
+ * Default credentials (Firebase + Supabase) will be retrieved from `getCredentials()` unless overriden here
36
+ *
37
+ * @param {Object} options Additional options to mutate behaviour (defaults to the main teraFy settings)
38
+ * @param {String} [options.firebaseApiKey] Firebase API key
39
+ * @param {String} [options.firebaseAuthDomain] Firebase authorized domain
40
+ * @param {String} [options.firebaseProjectId] Firebase project ID
41
+ * @param {String} [options.firebaseAppId] Firebase App ID
42
+ * @param {String} [options.supabaseUrl] Supabase URL
43
+ * @param {String} [options.supabaseKey] Supabase client key
44
+ *
45
+ * @returns {Promise} A Promise which will resolve when the init process has completed
46
+ */
47
+ async init(options) {
48
+ let settings = {
49
+ firebaseApiKey: null,
50
+ firebaseAuthDomain: null,
51
+ firebaseProjectId: null,
52
+ firebaseAppId: null,
53
+ supabaseUrl: null,
54
+ supabaseKey: null,
55
+ ...await this.getCredentials(),
56
+ ...options,
57
+ };
58
+
59
+ let emptyValues = Object.keys(settings).filter(k => k === null);
60
+ if (emptyValues.length > 0)
61
+ throw new Error('Firebase plugin requires mandatory options: ' + emptyValues.join(', '));
62
+
63
+ Syncro.firebase = Firebase({
64
+ apiKey: settings.firebaseApiKey,
65
+ authDomain: settings.firebaseAuthDomain,
66
+ projectId: settings.firebaseProjectId,
67
+ appId: settings.firebaseAppId,
68
+ });
69
+ Syncro.firestore = Firestore(this.firebase);
70
+
71
+ Syncro.supabase = Supabase(settings.supabaseUrl, settings.supabaseKey);
72
+ }
73
+
74
+
75
+ /**
76
+ * Mount the given namespace against `namespaces[name]`
77
+ *
78
+ * @param {'_PROJECT'|String} name The name/Syncro path of the namespace to mount (or '_PROJECT' for the project mountpoint)
79
+ *
80
+ * @returns {Promise} A promise which resolves when the operation has completed
81
+ */
82
+ _mountNamespace(name) {
83
+ let syncro; // The eventually bootstrapped Syncro object
84
+
85
+ return Promise.resolve()
86
+ .then(()=> this.requireProject())
87
+ .then(project => {
88
+ let path = name == '_PROJECT'
89
+ ? `projects::${project.id}`
90
+ : `project_namespaces::${project.id}::${name}`;
91
+
92
+ syncro = this.syncros[name] = new Syncro(path, {
93
+ debug: (...msg) => this.debug(`SYNCRO://${path}`, ...msg),
94
+ getReactive: this.getReactive, // Try to inherit this instances getReactive prop, otherwise Syncro will fall back to its default
95
+ });
96
+
97
+ // Perform the mount action
98
+ return syncro.mount();
99
+ })
100
+ .then(()=> { // Assign local state
101
+ this.namespaces[name] = syncro.value;
102
+ })
103
+ }
104
+
105
+
106
+ /**
107
+ * Unmount the given namespace from `namespaces[name]`
108
+ *
109
+ * @param {String} name The name/Syncro path of the namespace to unmount
110
+ *
111
+ * @returns {Promise} A promise which resolves when the operation has completed
112
+ */
113
+ _unmountNamespace(name) {
114
+ let syncro = this.syncros[name]; // Create local alias for Syncro before we detach it
115
+
116
+ // Detach local state
117
+ delete this.namespaces[name];
118
+ delete this.syncros[name];
119
+
120
+ return syncro.destroy(); // Trigger Syncro distruction
121
+ }
122
+ }
package/plugins/vue2.js CHANGED
@@ -1,17 +1,14 @@
1
- import {cloneDeep, debounce, isPlainObject} from 'lodash-es';
2
- import TeraFyPluginBase from './base.js';
1
+ import {cloneDeep} from 'lodash-es';
2
+ import TeraFyPluginFirebase from './firebase.js';
3
3
 
4
4
  /**
5
- * Vue2 observables plugin
6
- * Provides the `bindProjectState()` function for Vue based projects
5
+ * Vue@2 observables plugin
7
6
  *
8
7
  * This function is expected to be included via the `terafy.use(MODULE, OPTIONS)` syntax rather than directly
9
8
  *
10
- * @class TeraFyPluginVue
11
- * @param {Object} options Options when initalizing
12
- * @param {Vue} options.Vue Vue instance to bind against
9
+ * @class TeraFyPluginVue2
13
10
  *
14
- * @example Implementation within a Vue2 project `src/main.js`:
11
+ * @example Implementation within a Vue@2 project `src/main.js`:
15
12
  * // Include the main Tera-Fy core
16
13
  * import TeraFy from '@iebh/tera-fy';
17
14
  * import TerafyVue from '@iebh/tera-fy/plugins/vue2';
@@ -27,7 +24,7 @@ import TeraFyPluginBase from './base.js';
27
24
  * app.$mount("#app");
28
25
  * await terafy.init({app});
29
26
  */
30
- export default class TeraFyPluginVue2 extends TeraFyPluginBase {
27
+ export default class TeraFyPluginVue2 extends TeraFyPluginFirebase {
31
28
 
32
29
  /**
33
30
  * Local Vue@2 library to use, set during constuctor
@@ -37,6 +34,82 @@ export default class TeraFyPluginVue2 extends TeraFyPluginBase {
37
34
  Vue;
38
35
 
39
36
 
37
+ /**
38
+ * The bound, reactive state of the active TERA project
39
+ *
40
+ * @type {Object}
41
+ */
42
+ project = null;
43
+
44
+
45
+ /**
46
+ * Simple incrementor to ensure unique IDs for $watch expressions
47
+ *
48
+ * @type {Number{
49
+ */
50
+ reactiveId = 1001;
51
+
52
+
53
+ /**
54
+ * Install into Vue@2
55
+ *
56
+ * @param {Object} options Additional options to mutate behaviour, see TeraFyPluginFirebase
57
+ * @param {Object} options.app Root level Vue app to bind against
58
+ * @param {Vue} options.Vue Vue@2 instance to bind against
59
+ * @param {String} [options.globalName='$tera'] Global property to allocate this service as within Vue2
60
+ * @param {*...} [options...] see TeraFyPluginFirebase
61
+ *
62
+ * @returns {Promise} A Promise which will resolve when the init process has completed
63
+ */
64
+ async init(options) {
65
+ let settings = {
66
+ app: null,
67
+ Vue: null,
68
+ globalName: '$tera',
69
+ ...options,
70
+ };
71
+
72
+ if (!settings.Vue) throw new Error('Vue instance to use must be specified in init options as `Vue`');
73
+ this.Vue = settings.Vue;
74
+
75
+ if (!settings.app) throw new Error('Vue Root / App instance to use must be specified in init options as `app`');
76
+ this.app = settings.app;
77
+
78
+ // Make this module available globally
79
+ if (settings.globalName)
80
+ this.Vue.prototype[settings.globalName] = this;
81
+
82
+ await super.init(settings); // Initalize parent class Firebase functionality
83
+
84
+ this.project = await this.mountNamespace('_PROJECT');
85
+ }
86
+
87
+
88
+ /** @override */
89
+ getReactive(value) {
90
+ let doc = this.Vue.observable(value);
91
+
92
+ let watcherPath = `_teraFy_${this.reactiveId++}`;
93
+ this.app[watcherPath] = doc; // Attach onto app so we can use $watch later on
94
+
95
+ return {
96
+ doc,
97
+ setState(state) {
98
+ // Shallow copy all sub-keys into existing object (keeping the object pointer)
99
+ Object.entries(state || {})
100
+ .forEach(([k, v]) => doc[k] = v)
101
+ },
102
+ getState() {
103
+ return cloneDeep(doc);
104
+ },
105
+ watch: cb => {
106
+ this.app.$watch(watcherPath, cb, {deep: true});
107
+ },
108
+ };
109
+ }
110
+
111
+
112
+
40
113
  /**
41
114
  * Return a Vue Observable object that can be read/written which whose changes will transparently be written back to the TERA server instance
42
115
  *
@@ -175,89 +248,4 @@ export default class TeraFyPluginVue2 extends TeraFyPluginBase {
175
248
  return target;
176
249
  }
177
250
 
178
-
179
-
180
- /**
181
- * List of available projects for the current session
182
- * Initalized during constructor
183
- *
184
- * @type {VueReactive<Array<Object>>}
185
- */
186
- projects;
187
-
188
-
189
- /**
190
- * The bound, reactive state of a Vue project
191
- * When loaded this represents the state of a project as an object
192
- *
193
- * @type {Object}
194
- */
195
- state = null;
196
-
197
-
198
- /**
199
- * Install into Vue@2
200
- *
201
- * @param {Object} options Additional options to mutate behaviour (defaults to the main teraFy settings)
202
- * @param {Object} options.app Root level Vue app to bind against
203
- * @param {Vue} options.Vue Vue@2 instance to bind against
204
- * @param {String} [options.globalName='$tera'] Global property to allocate this service as within Vue2
205
- * @param {Boolean} [options.requireProject=true] Automatically call requireProject() prior to any operation
206
- * @param {Boolean} [options.subscribeState=true] Setup `vm.$tera.state` as a live binding on init
207
- * @param {Boolean} [options.subscribeList=true] Setup `vm.$tera.projects` as a list of accesible projects on init
208
- * @param {Objecct} [options.stateOptions] Options passed to `bindProjectState()` when setting up the main state
209
- *
210
- * @returns {Promise} A Promise which will resolve when the init process has completed
211
- */
212
- init(options) {
213
- let settings = {
214
- app: null,
215
- Vue: null,
216
- globalName: '$tera',
217
- requireProject: true,
218
- subscribeState: true,
219
- subscribeProjects: true,
220
- stateOptions: {
221
- read: true,
222
- write: true,
223
- },
224
- ...options,
225
- };
226
-
227
- if (!settings.Vue) throw new Error('Vue instance to use must be specified in init options as `Vue`');
228
- this.Vue = options.Vue;
229
-
230
- if (!this.settings.app) throw new Error('Need to specify the root level Vue2 app during init');
231
- settings.stateOptions.app = this.settings.app;
232
-
233
- // Create observable binding for projects
234
- this.projects = this.Vue.observable([])
235
-
236
- // Make this module available globally
237
- if (settings.globalName)
238
- this.Vue.prototype[settings.globalName] = this;
239
-
240
- // Bind `state` to the active project
241
- // Initialize state to null
242
- this.state = null;
243
-
244
- // this.statePromisable becomes the promise we are waiting on to resolve
245
- return Promise.resolve()
246
- .then(()=> settings.requireProject && this.requireProject())
247
- .then(()=> Promise.all([
248
- // Bind available project and wait on it
249
- settings.subscribeState && this.bindProjectState({
250
- ...settings.stateOptions,
251
- component: this.settings.app.$root,
252
- })
253
- .then(state => this.state = state)
254
- .then(()=> this.debug('INFO', 1, 'Loaded initial project state', this.state)),
255
-
256
- // Fetch available projects
257
- settings.subscribeProjects && this.getProjects()
258
- .then(projects => this.projects = this.Vue.observable(projects))
259
- .then(()=> this.debug('INFO', 2, 'Loaded project list', this.projects)),
260
- ]))
261
- .then(()=> this.debug('INFO', 1, 'Ready'))
262
- }
263
251
  }