@cocreate/elements 1.46.0 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocreate/elements",
3
- "version": "1.46.0",
3
+ "version": "2.0.0",
4
4
  "description": "Fetch content into any html element by using html5 attributes to query data stored in CoCreate headless CMS. If data is changed it will instantly reflect in html tags as changes are broadcasted in realtime",
5
5
  "keywords": [
6
6
  "elements",
@@ -22,30 +22,31 @@
22
22
  },
23
23
  "repository": {
24
24
  "type": "git",
25
- "url": "git+https://github.com/CoCreate-app/CoCreate-elements.git"
25
+ "url": "git+https://github.com/CoCreateJS/elements.git"
26
26
  },
27
27
  "author": "CoCreate LLC",
28
28
  "license": "AGPL-3.0",
29
29
  "bugs": {
30
- "url": "https://github.com/CoCreate-app/CoCreate-elements/issues"
30
+ "url": "https://github.com/CoCreateJS/elements/issues"
31
31
  },
32
- "homepage": "https://cocreate.app/docs/elements",
32
+ "homepage": "https://CoCreateJS.com/docs/elements",
33
33
  "funding": {
34
34
  "type": "GitHub Sponsors ❤",
35
- "url": "https://github.com/sponsors/CoCreate-app"
35
+ "url": "https://github.com/sponsors/CoCreateJS"
36
36
  },
37
37
  "main": "./src/index.js",
38
38
  "dependencies": {
39
39
  "@cocreate/actions": "^1.24.0",
40
- "@cocreate/crud-client": "^1.36.1",
40
+ "@cocreate/crud-client": "^1.37.0",
41
41
  "@cocreate/element-prototype": "^1.35.0",
42
- "@cocreate/filter": "^1.36.0",
43
- "@cocreate/observer": "^1.22.0",
44
- "@cocreate/render": "^1.50.0",
45
- "@cocreate/utils": "^1.52.0"
42
+ "@cocreate/filter": "^1.37.0",
43
+ "@cocreate/observer": "^1.23.0",
44
+ "@cocreate/render": "^1.51.0",
45
+ "@cocreate/socket-client": "^1.43.0",
46
+ "@cocreate/utils": "^1.54.0"
46
47
  },
47
48
  "devDependencies": {
48
- "@cocreate/webpack": "^1.8.0"
49
+ "@cocreate/webpack": "^1.9.0"
49
50
  },
50
51
  "allowScripts": {
51
52
  "@cocreate/actions@1.21.4": true,
@@ -8,8 +8,11 @@ import CRUD from "@cocreate/crud-client";
8
8
 
9
9
  import { elements, keys, debounce } from "./mapRegistry.js";
10
10
  import { getDataObject } from "./getDataHandler.js";
11
- import { setData } from "./setDataHandler.js";
12
- import { deriveSubscriptionKeys } from "./subscribeHandler.js";
11
+ import { setData, setDataType } from "./setDataHandler.js";
12
+ import {
13
+ deriveSubscriptionKeys,
14
+ getSubscription
15
+ } from "./subscribeHandler.js";
13
16
 
14
17
  /**
15
18
  * Reads data for one or more elements based on their dataKey scope.
@@ -26,8 +29,12 @@ export async function read(element, data, dataKey) {
26
29
  data = { ...existingData.dataKey.object };
27
30
  }
28
31
 
29
- if (!dataKey || !data.type) return;
30
- if (!data.$filter && (!data[data.type] || !data[data.type].length)) return;
32
+ if (!dataKey || !data?.type) {
33
+ return;
34
+ }
35
+ if (!data.filter && !data[data.type]) {
36
+ return;
37
+ }
31
38
 
32
39
  if (!(element instanceof HTMLCollection) && !Array.isArray(element))
33
40
  element = [element];
@@ -42,12 +49,10 @@ export async function read(element, data, dataKey) {
42
49
  if (
43
50
  isRead !== "false" &&
44
51
  !el.hasRead &&
45
- (!data.$filter || (data.$filter && data.$filter.isFilter !== false))
52
+ (!data.filter || (data.filter && data.filter.isFilter !== false))
46
53
  ) {
47
54
  el.hasRead = true;
48
55
  delay.elements.set(el, true);
49
- } else {
50
- // console.log("read skipped", el);
51
56
  }
52
57
  }
53
58
 
@@ -55,26 +60,39 @@ export async function read(element, data, dataKey) {
55
60
 
56
61
  clearTimeout(delay.timer);
57
62
  delay.timer = setTimeout(function () {
58
- if (!data.$filter && data.type === "object") {
59
- if (!data.object) return;
63
+ // Detach this batch before sending so reads queued while the request is
64
+ // in flight cannot mutate (or later delete) the same element map.
65
+ const els = Array.from(delay.elements?.keys() || []);
66
+ if (debounce.get(dataKey.string) === delay)
67
+ debounce.delete(dataKey.string);
68
+ clearTimeout(delay.timer);
69
+ delete delay.elements;
70
+ delete delay.timer;
71
+
72
+ if (!els.length) return;
73
+
74
+ if (!data.filter && data.type === "object") {
75
+ if (!data.object) {
76
+ return;
77
+ }
60
78
  else if (typeof data.object === "string")
61
79
  data.object = { _id: data.object };
62
80
  else if (Array.isArray(data.object)) {
63
81
  if (data.object.length)
64
82
  data.object = { _id: data.object[0]._id };
65
- } else if (!data.object._id.match(/^[0-9a-fA-F]{24}$/)) return;
83
+ } else if (!data.object._id.match(/^[0-9a-fA-F]{24}$/)) {
84
+ return;
85
+ }
66
86
  }
67
87
 
68
88
  data.method = data.type + ".read";
89
+ const subscription = getSubscription(data);
90
+ if (subscription) data.subscription = subscription.string;
69
91
 
70
- CRUD.send(data).then((responseData) => {
71
- debounce.delete(dataKey.string);
72
- let els = Array.from(delay.elements.keys());
73
- clearTimeout(delay.timer);
74
- delete delay.elements;
75
- delete delay.timer;
76
-
77
- setData(els, responseData);
92
+ CRUD.send(data).then(async (responseData) => {
93
+ if (responseData) {
94
+ await setData(els, responseData);
95
+ }
78
96
  });
79
97
  }, 500);
80
98
  }
@@ -111,8 +129,17 @@ export async function save(element, action) {
111
129
 
112
130
  value = await element.getValue();
113
131
  let key = element.getAttribute("key");
114
-
115
- if (typeof data[data.type] === "string")
132
+ if (data.type === "object" && !data.object) {
133
+ if (key == "{}") {
134
+ if (Array.isArray(value)) {
135
+ data[data.type] = value.map((v) => ({ ...v }));
136
+ } else
137
+ data[data.type] = value;
138
+ } else {
139
+ data[data.type] = { [key]: value };
140
+ }
141
+ }
142
+ else if (typeof data[data.type] === "string")
116
143
  if (key == "{}")
117
144
  data[data.type] = { _id: data[data.type], ...value };
118
145
  else data[data.type] = { _id: data[data.type], [key]: value };
@@ -178,24 +205,33 @@ export async function save(element, action) {
178
205
  array: data[i].array,
179
206
  key: data[i].key,
180
207
  object: data[i].object._id,
181
- value
208
+ value,
209
+ realtime: true,
210
+ commit: true
182
211
  });
183
212
  }
184
213
 
185
214
  // Derive and attach subscriptionKeys array (contains specific document key and list key)
215
+ const subscription = getSubscription(data[i]);
216
+ if (subscription) data[i].subscription = subscription.string;
186
217
  data[i].subscriptionKeys = deriveSubscriptionKeys(data[i]);
187
218
 
188
219
  data[i] = await CRUD.send(data[i]);
220
+ const errors = data[i]?.error;
221
+ if (!data[i] || (Array.isArray(errors) ? errors.length > 0 : !!errors)) {
222
+ const message = Array.isArray(errors)
223
+ ? errors.map((error) => error.message || String(error)).join("; ")
224
+ : errors?.message || String(errors || "Save failed");
225
+ throw new Error(message);
226
+ }
189
227
 
190
228
  responseDataList.push(data[i]);
191
229
 
192
230
  if (
193
231
  data[i] &&
194
- (data[i].method.endsWith(".create") ||
195
- (data[i].type !== "object" &&
196
- data[i].method.endsWith(".update")))
232
+ (data[i].method.endsWith(".create") || data[i].method.endsWith(".update"))
197
233
  ) {
198
- setDataType(element, data[i], action);
234
+ await setDataType(element, data[i], action);
199
235
  } else if (data[i]) {
200
236
  if (action) {
201
237
  action.element.dispatchEvent(
@@ -220,8 +256,49 @@ Actions.init([
220
256
  {
221
257
  name: "save",
222
258
  endEvent: "saved",
223
- callback: (action) => {
224
- if (action.form || action) save(action.form, action);
259
+ callback: async (action) => {
260
+ if (action.element.hasAttribute("save-query")) {
261
+ const targets = queryElements({
262
+ element: action.element,
263
+ prefix: "save"
264
+ });
265
+
266
+ if (!targets.length) {
267
+ action.abort({
268
+ code: "save-target-not-found",
269
+ message: "The save target could not be found.",
270
+ selector: action.element.attributes.getNamedItem("save-query")?.value
271
+ });
272
+ return;
273
+ }
274
+
275
+ try {
276
+ const result = [];
277
+ for (const target of targets) {
278
+ result.push(await save(target));
279
+ }
280
+ action.end({ result });
281
+ } catch (error) {
282
+ action.abort({
283
+ code: "save-failed",
284
+ message: "Unable to save the form.",
285
+ cause: error
286
+ });
287
+ console.error("Save action failed:", error);
288
+ }
289
+ return;
290
+ }
291
+
292
+ try {
293
+ await save(action.form || action.element, action);
294
+ } catch (error) {
295
+ action.abort({
296
+ code: "save-failed",
297
+ message: "Unable to save the form.",
298
+ cause: error
299
+ });
300
+ console.error("Save action failed:", error);
301
+ }
225
302
  }
226
303
  },
227
304
  {
@@ -238,34 +315,63 @@ Actions.init([
238
315
  elements = [action.element];
239
316
  }
240
317
 
318
+ const groups = new Map();
241
319
  for (let i = 0; i < elements.length; i++) {
242
320
  const data = getDataObject(elements[i]);
243
- if (!data) return;
321
+ if (!data) continue;
244
322
  data.method = data.type + ".delete";
245
323
 
246
- if (elements[i].renderValue) {
247
- let selected = elements[i].querySelectorAll(".selected");
248
- data[data.type] = [];
249
- for (let j = 0; j < selected.length; j++) {
250
- let attribute = selected[j].getAttribute(data.type);
251
- if (attribute) {
252
- attribute = attribute.split(",");
253
- for (let k = 0; k < attribute.length; k++) {
254
- if (data.type === "object")
255
- data[data.type].push({ _id: attribute[k] });
256
- else {
257
- data[data.type].push(attribute[k]);
258
- }
259
- }
260
- }
261
- }
262
- } else if (
324
+ if (
263
325
  data.type === "object" &&
264
326
  typeof data[data.type] === "string"
265
327
  )
266
328
  data[data.type] = { _id: data[data.type] };
267
329
 
268
- // Attach subscriptionKeys before sending delete payload
330
+ if (data.type !== "object") {
331
+ groups.set(Symbol(), data);
332
+ continue;
333
+ }
334
+
335
+ const groupKey = JSON.stringify({
336
+ storage: data.storage || "",
337
+ database: data.database || "",
338
+ array: data.array
339
+ });
340
+ const objects = Array.isArray(data.object)
341
+ ? data.object
342
+ : [data.object];
343
+ let group = groups.get(groupKey);
344
+
345
+ if (!group) {
346
+ group = {
347
+ ...data,
348
+ object: [],
349
+ objectIds: new Set()
350
+ };
351
+ groups.set(groupKey, group);
352
+ }
353
+
354
+ for (const object of objects) {
355
+ if (!object || !object._id || group.objectIds.has(object._id))
356
+ continue;
357
+ group.objectIds.add(object._id);
358
+ group.object.push({ _id: object._id });
359
+ }
360
+ }
361
+
362
+ for (const data of groups.values()) {
363
+ if (data.objectIds) {
364
+ delete data.objectIds;
365
+ if (!data.object.length) continue;
366
+ }
367
+
368
+ // Subscribe the requester at collection scope. A grouped object
369
+ // array is a delete payload, not a subscribable document key.
370
+ const subscriptionData = { ...data };
371
+ delete subscriptionData.object;
372
+ const subscription = getSubscription(subscriptionData);
373
+ if (subscription)
374
+ data.subscription = subscription.string;
269
375
  data.subscriptionKeys = deriveSubscriptionKeys(data);
270
376
 
271
377
  let response = await CRUD.send(data);
@@ -279,4 +385,4 @@ Actions.init([
279
385
  }
280
386
  ]);
281
387
 
282
- export default { read, save };
388
+ export default { read, save };
@@ -0,0 +1,7 @@
1
+ // CRUD runtime. Loaded by data selectors, CRUD actions, or an Elements API call.
2
+ import "@cocreate/element-prototype";
3
+
4
+ export { init } from "./initHandler.js";
5
+ export { read, save } from "./crudHandler.js";
6
+ export { getData } from "./getDataHandler.js";
7
+ export { setDataType } from "./setDataHandler.js";
@@ -0,0 +1,54 @@
1
+ import { getAttributes, checkValue } from "@cocreate/utils";
2
+
3
+ export const environment = localStorage.getItem("environment");
4
+
5
+ /**
6
+ * Parses and normalizes HTML attributes from an element into a canonical CRUD data object.
7
+ * Infers execution type (object, index, array, database, storage, data) dynamically.
8
+ *
9
+ * @param {Element} element
10
+ * @returns {Object|undefined} Normalized data payload object
11
+ */
12
+ export function getDataObject(element) {
13
+ const data = getAttributes(element);
14
+ const crudType = ["storage", "database", "array", "index", "object"];
15
+
16
+ for (let i = 0; i < crudType.length; i++) {
17
+ if (!checkValue(data[crudType[i]])) return;
18
+
19
+ if (data[crudType[i]] && data[crudType[i]].includes(",")) {
20
+ const array = data[crudType[i]].split(",");
21
+ data[crudType[i]] = [];
22
+
23
+ for (let j = 0; j < array.length; j++) {
24
+ const item = array[j].trim();
25
+ if (crudType[i] === "object") {
26
+ data[crudType[i]].push({ _id: item });
27
+ } else {
28
+ data[crudType[i]].push(item);
29
+ }
30
+ }
31
+ }
32
+ }
33
+
34
+ if (data.object || data.object === "" || data.key || data.key === "") {
35
+ if (!data.array || (data.array && !data.array.length)) return;
36
+ data.type = "object";
37
+ } else if (data.index || data.index === "") {
38
+ if (!data.array || (data.array && !data.array.length)) return;
39
+ data.type = "index";
40
+ } else if (data.array || data.array === "") data.type = "array";
41
+ else if (data.database || data.database === "") data.type = "database";
42
+ else if (data.storage || data.storage === "") data.type = "storage";
43
+ else if (data.data) data.type = "data";
44
+
45
+ if (
46
+ environment &&
47
+ !data.database &&
48
+ ["array", "index", "object"].includes(data.type)
49
+ )
50
+ data.database = environment;
51
+
52
+ delete data.isRealtime;
53
+ return data;
54
+ }