@orieljs/live 0.1.0-alpha.5 → 0.1.0-alpha.6

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/NOTICE CHANGED
@@ -7,3 +7,6 @@ This product includes software developed by the Oriel project
7
7
  Oriel is licensed under the Apache License, Version 2.0 (see LICENSE).
8
8
  The Oriel name and logo are not covered by that license; see TRADEMARKS.md
9
9
  (https://github.com/orieljs/framework/blob/main/TRADEMARKS.md).
10
+
11
+ @orieljs/support (and @orieljs/framework, which bundles it) includes transliteration data from
12
+ voku/portable-ascii (https://github.com/voku/portable-ascii), Copyright (C) 2019 Lars Moelleken, MIT License.
package/README.md CHANGED
@@ -33,6 +33,43 @@ Route.get('/counter', Counter)
33
33
 
34
34
  Embed a component in any view with `<Live component={Counter} />`. In the browser, components are driven by `@orieljs/live-client`.
35
35
 
36
+ ## Typed actions
37
+
38
+ `this.$action(method, ...arguments)` writes an action for an `o:` directive, typed against the method:
39
+
40
+ ```tsx
41
+ <button type="button" o:click={this.$action(this.revokeAdmin, user.id)}>
42
+ Revoke admin
43
+ </button>
44
+ ```
45
+
46
+ This renders `o:click="revokeAdmin(5)"`, and when `revokeAdmin(id: number)` takes a number,
47
+ `this.$action(this.revokeAdmin, String(user.id))` is a type error. The arguments are written as JSON, so pass ids and
48
+ plain values (strings, numbers, booleans, `null`, arrays and plain objects): a model arrives as its attributes and a
49
+ date as its ISO string, so pass `post.id`, not `post`. The action contains double quotes, so use it as a TSX attribute
50
+ value, which escapes them, not inside an HTML string you write by hand. Rendering throws when the method isn't one of
51
+ the component's actions (a hook, or a `#private` or `@Locked` method). The types check what your pages send; a crafted
52
+ request can still send anything, so an action checks what it is given, as a controller does.
53
+
54
+ ## Confirming an action
55
+
56
+ `o:confirm` asks before an action runs, as Livewire's `wire:confirm`:
57
+
58
+ ```tsx
59
+ <button type="button" class="btn btn-danger" o:click={`delete(${tag.id})`} o:confirm="Delete this tag?">
60
+ Delete
61
+ </button>
62
+ ```
63
+
64
+ The browser's confirm dialog shows the message (an empty `o:confirm` asks "Are you sure?", and `\n` is a line break),
65
+ and the action runs only when it is accepted. `o:confirm-prompt="Type DELETE to confirm|DELETE"` (`o:confirm.prompt`
66
+ in plain HTML) asks with a prompt instead, and runs the action only when the answer is exactly the text after the `|`.
67
+ It works on any element with an `o:` action: `o:click`, `o:submit` on a form, `o:keydown-enter`, and the others. A
68
+ dismissed dialog stops the event, so the actions of the elements around it don't run either. `o:confirm` guards the
69
+ actions of the element it sits on: on a form, put it on the `<form>` with the `o:submit`. A button inside a form with
70
+ its own `o:click` needs `type="button"` or `-prevent` (`o:click-prevent`), or the browser still submits the form when
71
+ the dialog is dismissed.
72
+
36
73
  ## Pagination
37
74
 
38
75
  ```tsx
@@ -77,6 +114,24 @@ updates. The list is `live.persistent_middleware` in `config/live.ts`.
77
114
  A status an action flashes (`session().flash('status', 'Saved.')`) shows in that update's response only, unless the
78
115
  action redirects or navigates: then it is there on the next page. Component state is unaffected.
79
116
 
117
+ ## Status messages announced again
118
+
119
+ Screen readers announce a status region (`role="status"`) when its text changes. When an action brings the same
120
+ message again ("Saved" after a second save), the page doesn't change and nothing is announced. Add `o:announce` to the
121
+ region and Live takes the message out and puts it back a moment later, so it is announced again:
122
+
123
+ ```tsx
124
+ <p role="status" o:announce>
125
+ {this.#saved ? 'Saved' : ''}
126
+ </p>
127
+ ```
128
+
129
+ Only actions do this, and an `o:poll` that calls a method counts as one: an update that only syncs `o:model` values,
130
+ refreshes or delivers an event leaves the region alone. Keep a status for one response (a `#private` field, or a
131
+ flashed `status`), so that any other update clears it; a status kept in a public property is announced again by every
132
+ action until it changes, which in a polled component is every tick. Keep an `o:announce` region to text: its children
133
+ are taken out for 100 ms, so a button or input inside it would lose focus.
134
+
80
135
  ## Known limitations
81
136
 
82
137
  - `o:navigate` does not run scripts in the new page.
package/dist/index.d.ts CHANGED
@@ -74,6 +74,13 @@ declare abstract class Component {
74
74
  reset(...properties: string[]): void;
75
75
  /** Keeps the current HTML: the response carries the new state but no `html` effect. */
76
76
  skipRender(): void;
77
+ /**
78
+ * An action for an `o:` directive, typed against its method: `o:click={this.$action(this.delete, tag.id)}` writes
79
+ * `delete(5)`, and a string where `delete` takes a number is a type error. The arguments are written as JSON, the
80
+ * trailing `undefined` ones left out so the method's defaults apply. The method must be one of this component's
81
+ * actions (a public method of its class, not a hook and not @Locked); anything else throws while rendering.
82
+ */
83
+ $action<P extends unknown[]>(method: (...params: P) => unknown, ...params: NoInfer<P>): string;
77
84
  /** A full-page component as a route action: Route.get('/counter', Counter). Route parameters go to mount(). */
78
85
  __invoke(): Promise<unknown>;
79
86
  }
@@ -125,6 +132,9 @@ declare class Testable<C extends Component = Component> {
125
132
  assertDontSee(...values: string[]): this;
126
133
  /** Each value appears in the rendered HTML as written (not escaped). */
127
134
  assertSeeHtml(...values: string[]): this;
135
+ /** Each value, HTML-escaped, appears in the rendered text: the HTML with its tags stripped (Livewire's assertSeeText). */
136
+ assertSeeText(...values: string[]): this;
137
+ assertDontSeeText(...values: string[]): this;
128
138
  assertDispatched(name: string, params?: Record<string, unknown>): this;
129
139
  assertNotDispatched(name: string): this;
130
140
  /** The error bag has errors for every key given (or any error at all with no keys). */
package/dist/index.js CHANGED
@@ -2,6 +2,50 @@
2
2
  import { LiveManager, UrlGenerator, ValidationFactory } from "@orieljs/contracts";
3
3
  import { inject } from "@orieljs/container";
4
4
 
5
+ // errors.ts
6
+ var LiveError = class extends Error {
7
+ name = "LiveError";
8
+ kind = "Server";
9
+ status = 500;
10
+ };
11
+ var CorruptComponentPayloadError = class extends LiveError {
12
+ name = "CorruptComponentPayloadError";
13
+ kind = "Authorization";
14
+ status = 403;
15
+ constructor(component = "unknown") {
16
+ super(`Live encountered corrupt data when trying to hydrate the [${component}] component.`);
17
+ }
18
+ };
19
+ var MethodNotCallableError = class extends LiveError {
20
+ name = "MethodNotCallableError";
21
+ kind = "Authorization";
22
+ status = 403;
23
+ constructor(method, component) {
24
+ super(`Unable to call component method. Public method [${method}] not found on component [${component}].`);
25
+ }
26
+ };
27
+ var PropertyNotUpdatableError = class extends LiveError {
28
+ name = "PropertyNotUpdatableError";
29
+ kind = "Authorization";
30
+ status = 403;
31
+ constructor(path, component) {
32
+ super(`Cannot update property [${path}] on component [${component}]: it is not a public, unlocked property.`);
33
+ }
34
+ };
35
+ var LiveComponentNotFoundError = class extends LiveError {
36
+ name = "LiveComponentNotFoundError";
37
+ kind = "Http";
38
+ status = 404;
39
+ constructor(component) {
40
+ super(`Unable to find component: [${component}].`);
41
+ }
42
+ };
43
+ var InvalidLiveRequestError = class extends LiveError {
44
+ name = "InvalidLiveRequestError";
45
+ kind = "Http";
46
+ status = 400;
47
+ };
48
+
5
49
  // metadata.ts
6
50
  Symbol.metadata ??= /* @__PURE__ */ Symbol.for("Symbol.metadata");
7
51
  var LISTENERS = /* @__PURE__ */ Symbol.for("oriel:live.listeners");
@@ -75,6 +119,153 @@ function computedOf(cls) {
75
119
  return /* @__PURE__ */ new Set([...metadataOf(cls)[COMPUTED] ?? [], ...staticsOf(cls, "computed").flat()]);
76
120
  }
77
121
 
122
+ // security.ts
123
+ import { Collection as Collection2 } from "@orieljs/collections";
124
+
125
+ // synthesizers.ts
126
+ import { Collection } from "@orieljs/collections";
127
+ function isModel(value) {
128
+ if (typeof value !== "object" || value === null) return false;
129
+ const candidate = value;
130
+ return typeof candidate.getKey === "function" && typeof candidate.getKeyName === "function" && typeof candidate.getTable === "function" && typeof candidate.exists === "boolean";
131
+ }
132
+ var isPlainObject = (value) => {
133
+ const proto = Object.getPrototypeOf(value);
134
+ return proto === Object.prototype || proto === null;
135
+ };
136
+ var isTuple = (value) => Array.isArray(value) && value.length === 2 && typeof value[1] === "object" && value[1] !== null && !Array.isArray(value[1]) && typeof value[1].s === "string";
137
+ function classNameOf(registry, cls) {
138
+ return registry.nameOf(cls) ?? registry.register(cls);
139
+ }
140
+ function dehydrate(value, registry, path, seen = /* @__PURE__ */ new Set()) {
141
+ switch (typeof value) {
142
+ case "undefined":
143
+ return [null, { s: "u" }];
144
+ case "string":
145
+ case "boolean":
146
+ return value;
147
+ case "number":
148
+ if (!Number.isFinite(value)) throw new LiveError(`Property [${path}] is ${value}: Live state must be JSON (finite numbers only).`);
149
+ return value;
150
+ case "bigint":
151
+ case "symbol":
152
+ case "function":
153
+ throw new LiveError(`Property [${path}] holds a ${typeof value}: Live state may only hold primitives, arrays, plain objects, Dates, models and collections.`);
154
+ }
155
+ if (value === null) return null;
156
+ const object = value;
157
+ if (seen.has(object)) throw new LiveError(`Property [${path}] contains a circular reference.`);
158
+ seen.add(object);
159
+ try {
160
+ if (object instanceof Date) return [object.toISOString(), { s: "dt" }];
161
+ if (Array.isArray(object)) return [object.map((item, i) => dehydrate(item, registry, `${path}.${i}`, seen)), { s: "arr" }];
162
+ if (isModel(object)) {
163
+ const name = classNameOf(registry, object.constructor);
164
+ if (!object.exists) throw new LiveError(`Property [${path}] holds an unsaved [${name}] model: only saved models can be kept between requests.`);
165
+ return [null, { s: "mdl", class: name, key: object.getKey() }];
166
+ }
167
+ if (object instanceof Collection) {
168
+ const items = object.all();
169
+ const first = items[0];
170
+ if (items.length > 0 && isModel(first) && items.every((item) => isModel(item) && item.constructor === first.constructor)) {
171
+ for (const [i, item] of items.entries()) {
172
+ if (!item.exists) throw new LiveError(`Property [${path}.${i}] holds an unsaved model.`);
173
+ }
174
+ const name = classNameOf(registry, first.constructor);
175
+ return [null, { s: "elcln", class: name, keys: items.map((item) => item.getKey()) }];
176
+ }
177
+ return [items.map((item, i) => dehydrate(item, registry, `${path}.${i}`, seen)), { s: "clctn" }];
178
+ }
179
+ if (isPlainObject(object)) {
180
+ const out = {};
181
+ for (const [key, item] of Object.entries(object)) out[key] = dehydrate(item, registry, `${path}.${key}`, seen);
182
+ return [out, { s: "obj" }];
183
+ }
184
+ const type = object.constructor?.name || "object";
185
+ throw new LiveError(
186
+ `Property [${path}] holds a [${type}] instance: Live state may only hold primitives, arrays, plain objects, Dates, models and collections.`
187
+ );
188
+ } finally {
189
+ seen.delete(object);
190
+ }
191
+ }
192
+ function modelClass(registry, name) {
193
+ if (typeof name !== "string" || !registry.has(name)) throw new LiveError(`Unable to hydrate the [${String(name)}] model: it is not registered.`);
194
+ return registry.resolve(name);
195
+ }
196
+ async function hydrate(value, registry) {
197
+ if (!isTuple(value)) return value;
198
+ const [inner, meta] = value;
199
+ switch (meta.s) {
200
+ case "u":
201
+ return void 0;
202
+ case "dt":
203
+ return new Date(String(inner));
204
+ case "arr":
205
+ return Promise.all(inner.map((item) => hydrate(item, registry)));
206
+ case "clctn":
207
+ return new Collection(await Promise.all(inner.map((item) => hydrate(item, registry))));
208
+ case "obj": {
209
+ const out = {};
210
+ for (const [key, item] of Object.entries(inner)) {
211
+ Object.defineProperty(out, key, { value: await hydrate(item, registry), enumerable: true, writable: true, configurable: true });
212
+ }
213
+ return out;
214
+ }
215
+ case "mdl":
216
+ return modelClass(registry, meta.class).findOrFail(meta.key);
217
+ case "elcln": {
218
+ const cls = modelClass(registry, meta.class);
219
+ const keys = meta.keys ?? [];
220
+ const keyName = new cls().getKeyName();
221
+ const found = [...await cls.query().whereIn(keyName, keys).get()];
222
+ const byKey = new Map(found.map((model) => [String(model.getKey()), model]));
223
+ return new Collection(keys.map((key) => byKey.get(String(key))).filter((model) => model !== void 0));
224
+ }
225
+ }
226
+ throw new LiveError(`Unknown snapshot value type [${String(meta.s)}].`);
227
+ }
228
+
229
+ // security.ts
230
+ var HOOKS = /* @__PURE__ */ new Set(["constructor", "render", "mount", "boot", "booted", "hydrate", "dehydrate", "updating", "updated", "rendering", "rendered", "exception"]);
231
+ var HOOK_PREFIX = /^(updating|updated|hydrate|dehydrate)[A-Z_]/;
232
+ var UNSAFE_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
233
+ var baseApi;
234
+ var isBaseApi = (name) => (baseApi ??= new Set(Object.getOwnPropertyNames(Component.prototype))).has(name);
235
+ var INDEX = /^(?:0|[1-9]\d*)$/;
236
+ function isArraySlot(array, segment) {
237
+ return INDEX.test(segment) && Number(segment) <= array.length;
238
+ }
239
+ function isCallable(instance, method) {
240
+ if (method === "" || method.startsWith("_") || method.startsWith("$") || method.startsWith("#")) return false;
241
+ if (HOOKS.has(method) || HOOK_PREFIX.test(method) || isBaseApi(method)) return false;
242
+ if (lockedOf(instance.constructor).has(method)) return false;
243
+ const isBase = (proto) => proto === Component.prototype || proto === Object.prototype;
244
+ for (let proto = Object.getPrototypeOf(instance); proto !== null && !isBase(proto); proto = Object.getPrototypeOf(proto)) {
245
+ const descriptor = Object.getOwnPropertyDescriptor(proto, method);
246
+ if (descriptor !== void 0) return typeof descriptor.value === "function";
247
+ }
248
+ return false;
249
+ }
250
+ function isUpdatable(instance, path) {
251
+ const segments = path.split(".");
252
+ const root = segments[0];
253
+ if (root === "" || root.startsWith("_") || segments.some((segment) => segment === "" || UNSAFE_SEGMENTS.has(segment))) return false;
254
+ if (!Object.hasOwn(instance, root)) return false;
255
+ const descriptor = Object.getOwnPropertyDescriptor(instance, root);
256
+ if (!descriptor.enumerable || descriptor.get !== void 0) return false;
257
+ const cls = instance.constructor;
258
+ if (lockedOf(cls).has(root) || computedOf(cls).has(root)) return false;
259
+ let node = descriptor.value;
260
+ for (const segment of segments.slice(1)) {
261
+ if (isModel(node) || node instanceof Collection2) return false;
262
+ if (Array.isArray(node) && !isArraySlot(node, segment)) return false;
263
+ if (typeof node !== "object" || node === null || !Object.hasOwn(node, segment)) return true;
264
+ node = node[segment];
265
+ }
266
+ return !isModel(node) && !(node instanceof Collection2);
267
+ }
268
+
78
269
  // Component.ts
79
270
  var COMPONENT = /* @__PURE__ */ Symbol.for("oriel:live.component");
80
271
  var states = /* @__PURE__ */ new WeakMap();
@@ -206,6 +397,23 @@ var Component = class {
206
397
  skipRender() {
207
398
  stateOf(this).skipRender = true;
208
399
  }
400
+ /**
401
+ * An action for an `o:` directive, typed against its method: `o:click={this.$action(this.delete, tag.id)}` writes
402
+ * `delete(5)`, and a string where `delete` takes a number is a type error. The arguments are written as JSON, the
403
+ * trailing `undefined` ones left out so the method's defaults apply. The method must be one of this component's
404
+ * actions (a public method of its class, not a hook and not @Locked); anything else throws while rendering.
405
+ */
406
+ $action(method, ...params) {
407
+ const name = typeof method === "function" ? method.name : "";
408
+ if (name === "" || this[name] !== method || !isCallable(this, name)) {
409
+ throw new LiveError(
410
+ `[${name === "" ? "anonymous function" : name}] is not an action of [${this.getName() || this.constructor.name}]: pass one of its public methods, as in this.$action(this.save).`
411
+ );
412
+ }
413
+ let count = params.length;
414
+ while (count > 0 && params[count - 1] === void 0) count--;
415
+ return count === 0 ? name : `${name}(${params.slice(0, count).map((param) => JSON.stringify(param) ?? "null").join(", ")})`;
416
+ }
209
417
  /** A full-page component as a route action: Route.get('/counter', Counter). Route parameters go to mount(). */
210
418
  async __invoke() {
211
419
  return inject(LiveManager).page(this);
@@ -238,55 +446,14 @@ function trackFlash(session, { saved = true } = {}) {
238
446
  };
239
447
  }
240
448
 
241
- // errors.ts
242
- var LiveError = class extends Error {
243
- name = "LiveError";
244
- kind = "Server";
245
- status = 500;
246
- };
247
- var CorruptComponentPayloadError = class extends LiveError {
248
- name = "CorruptComponentPayloadError";
249
- kind = "Authorization";
250
- status = 403;
251
- constructor(component = "unknown") {
252
- super(`Live encountered corrupt data when trying to hydrate the [${component}] component.`);
253
- }
254
- };
255
- var MethodNotCallableError = class extends LiveError {
256
- name = "MethodNotCallableError";
257
- kind = "Authorization";
258
- status = 403;
259
- constructor(method, component) {
260
- super(`Unable to call component method. Public method [${method}] not found on component [${component}].`);
261
- }
262
- };
263
- var PropertyNotUpdatableError = class extends LiveError {
264
- name = "PropertyNotUpdatableError";
265
- kind = "Authorization";
266
- status = 403;
267
- constructor(path, component) {
268
- super(`Cannot update property [${path}] on component [${component}]: it is not a public, unlocked property.`);
269
- }
270
- };
271
- var LiveComponentNotFoundError = class extends LiveError {
272
- name = "LiveComponentNotFoundError";
273
- kind = "Http";
274
- status = 404;
275
- constructor(component) {
276
- super(`Unable to find component: [${component}].`);
277
- }
278
- };
279
- var InvalidLiveRequestError = class extends LiveError {
280
- name = "InvalidLiveRequestError";
281
- kind = "Http";
282
- status = 400;
283
- };
284
-
285
449
  // html.ts
286
450
  var ESCAPES = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#039;" };
287
451
  function escapeAttribute(value) {
288
452
  return value.replace(/[&<>"']/g, (char) => ESCAPES[char]);
289
453
  }
454
+ function stripTags(html) {
455
+ return html.replace(/<[^>]*>/g, "");
456
+ }
290
457
  var VOID = /* @__PURE__ */ new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "source", "track", "wbr"]);
291
458
  var RAW_TEXT = /* @__PURE__ */ new Set(["script", "style", "textarea", "title"]);
292
459
  var TOKEN = /<\/([A-Za-z][\w:-]*)\s*>|<([A-Za-z][\w:-]*)(?:\s+[^\s"'<>/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?)*\s*(\/?)>/g;
@@ -441,6 +608,17 @@ var Testable = class _Testable {
441
608
  for (const value of values) if (!this.html().includes(value)) fail(`Failed asserting that [${value}] is in the rendered HTML.`, this.html(), value);
442
609
  return this;
443
610
  }
611
+ /** Each value, HTML-escaped, appears in the rendered text: the HTML with its tags stripped (Livewire's assertSeeText). */
612
+ assertSeeText(...values) {
613
+ const text = stripTags(this.html());
614
+ for (const value of values) if (!text.includes(escapeAttribute(value))) fail(`Failed asserting that [${value}] is in the rendered text.`, text, value);
615
+ return this;
616
+ }
617
+ assertDontSeeText(...values) {
618
+ const text = stripTags(this.html());
619
+ for (const value of values) if (text.includes(escapeAttribute(value))) fail(`Failed asserting that [${value}] is not in the rendered text.`, text, value);
620
+ return this;
621
+ }
444
622
  assertDispatched(name, params) {
445
623
  const dispatched = (this.#effects.dispatches ?? []).filter((event) => event.name === name);
446
624
  if (dispatched.length === 0) fail(`Failed asserting that an event [${name}] was dispatched.`, this.#effects.dispatches ?? [], name);
@@ -690,152 +868,6 @@ async function throughMiddleware(container, request, pass, destination) {
690
868
  }
691
869
  }
692
870
 
693
- // security.ts
694
- import { Collection as Collection2 } from "@orieljs/collections";
695
-
696
- // synthesizers.ts
697
- import { Collection } from "@orieljs/collections";
698
- function isModel(value) {
699
- if (typeof value !== "object" || value === null) return false;
700
- const candidate = value;
701
- return typeof candidate.getKey === "function" && typeof candidate.getKeyName === "function" && typeof candidate.getTable === "function" && typeof candidate.exists === "boolean";
702
- }
703
- var isPlainObject = (value) => {
704
- const proto = Object.getPrototypeOf(value);
705
- return proto === Object.prototype || proto === null;
706
- };
707
- var isTuple = (value) => Array.isArray(value) && value.length === 2 && typeof value[1] === "object" && value[1] !== null && !Array.isArray(value[1]) && typeof value[1].s === "string";
708
- function classNameOf(registry, cls) {
709
- return registry.nameOf(cls) ?? registry.register(cls);
710
- }
711
- function dehydrate(value, registry, path, seen = /* @__PURE__ */ new Set()) {
712
- switch (typeof value) {
713
- case "undefined":
714
- return [null, { s: "u" }];
715
- case "string":
716
- case "boolean":
717
- return value;
718
- case "number":
719
- if (!Number.isFinite(value)) throw new LiveError(`Property [${path}] is ${value}: Live state must be JSON (finite numbers only).`);
720
- return value;
721
- case "bigint":
722
- case "symbol":
723
- case "function":
724
- throw new LiveError(`Property [${path}] holds a ${typeof value}: Live state may only hold primitives, arrays, plain objects, Dates, models and collections.`);
725
- }
726
- if (value === null) return null;
727
- const object = value;
728
- if (seen.has(object)) throw new LiveError(`Property [${path}] contains a circular reference.`);
729
- seen.add(object);
730
- try {
731
- if (object instanceof Date) return [object.toISOString(), { s: "dt" }];
732
- if (Array.isArray(object)) return [object.map((item, i) => dehydrate(item, registry, `${path}.${i}`, seen)), { s: "arr" }];
733
- if (isModel(object)) {
734
- const name = classNameOf(registry, object.constructor);
735
- if (!object.exists) throw new LiveError(`Property [${path}] holds an unsaved [${name}] model: only saved models can be kept between requests.`);
736
- return [null, { s: "mdl", class: name, key: object.getKey() }];
737
- }
738
- if (object instanceof Collection) {
739
- const items = object.all();
740
- const first = items[0];
741
- if (items.length > 0 && isModel(first) && items.every((item) => isModel(item) && item.constructor === first.constructor)) {
742
- for (const [i, item] of items.entries()) {
743
- if (!item.exists) throw new LiveError(`Property [${path}.${i}] holds an unsaved model.`);
744
- }
745
- const name = classNameOf(registry, first.constructor);
746
- return [null, { s: "elcln", class: name, keys: items.map((item) => item.getKey()) }];
747
- }
748
- return [items.map((item, i) => dehydrate(item, registry, `${path}.${i}`, seen)), { s: "clctn" }];
749
- }
750
- if (isPlainObject(object)) {
751
- const out = {};
752
- for (const [key, item] of Object.entries(object)) out[key] = dehydrate(item, registry, `${path}.${key}`, seen);
753
- return [out, { s: "obj" }];
754
- }
755
- const type = object.constructor?.name || "object";
756
- throw new LiveError(
757
- `Property [${path}] holds a [${type}] instance: Live state may only hold primitives, arrays, plain objects, Dates, models and collections.`
758
- );
759
- } finally {
760
- seen.delete(object);
761
- }
762
- }
763
- function modelClass(registry, name) {
764
- if (typeof name !== "string" || !registry.has(name)) throw new LiveError(`Unable to hydrate the [${String(name)}] model: it is not registered.`);
765
- return registry.resolve(name);
766
- }
767
- async function hydrate(value, registry) {
768
- if (!isTuple(value)) return value;
769
- const [inner, meta] = value;
770
- switch (meta.s) {
771
- case "u":
772
- return void 0;
773
- case "dt":
774
- return new Date(String(inner));
775
- case "arr":
776
- return Promise.all(inner.map((item) => hydrate(item, registry)));
777
- case "clctn":
778
- return new Collection(await Promise.all(inner.map((item) => hydrate(item, registry))));
779
- case "obj": {
780
- const out = {};
781
- for (const [key, item] of Object.entries(inner)) {
782
- Object.defineProperty(out, key, { value: await hydrate(item, registry), enumerable: true, writable: true, configurable: true });
783
- }
784
- return out;
785
- }
786
- case "mdl":
787
- return modelClass(registry, meta.class).findOrFail(meta.key);
788
- case "elcln": {
789
- const cls = modelClass(registry, meta.class);
790
- const keys = meta.keys ?? [];
791
- const keyName = new cls().getKeyName();
792
- const found = [...await cls.query().whereIn(keyName, keys).get()];
793
- const byKey = new Map(found.map((model) => [String(model.getKey()), model]));
794
- return new Collection(keys.map((key) => byKey.get(String(key))).filter((model) => model !== void 0));
795
- }
796
- }
797
- throw new LiveError(`Unknown snapshot value type [${String(meta.s)}].`);
798
- }
799
-
800
- // security.ts
801
- var HOOKS = /* @__PURE__ */ new Set(["constructor", "render", "mount", "boot", "booted", "hydrate", "dehydrate", "updating", "updated", "rendering", "rendered", "exception"]);
802
- var HOOK_PREFIX = /^(updating|updated|hydrate|dehydrate)[A-Z_]/;
803
- var UNSAFE_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
804
- var BASE_API = new Set(Object.getOwnPropertyNames(Component.prototype));
805
- var INDEX = /^(?:0|[1-9]\d*)$/;
806
- function isArraySlot(array, segment) {
807
- return INDEX.test(segment) && Number(segment) <= array.length;
808
- }
809
- function isCallable(instance, method) {
810
- if (method === "" || method.startsWith("_") || method.startsWith("$") || method.startsWith("#")) return false;
811
- if (HOOKS.has(method) || HOOK_PREFIX.test(method) || BASE_API.has(method)) return false;
812
- if (lockedOf(instance.constructor).has(method)) return false;
813
- const isBase = (proto) => proto === Component.prototype || proto === Object.prototype;
814
- for (let proto = Object.getPrototypeOf(instance); proto !== null && !isBase(proto); proto = Object.getPrototypeOf(proto)) {
815
- const descriptor = Object.getOwnPropertyDescriptor(proto, method);
816
- if (descriptor !== void 0) return typeof descriptor.value === "function";
817
- }
818
- return false;
819
- }
820
- function isUpdatable(instance, path) {
821
- const segments = path.split(".");
822
- const root = segments[0];
823
- if (root === "" || root.startsWith("_") || segments.some((segment) => segment === "" || UNSAFE_SEGMENTS.has(segment))) return false;
824
- if (!Object.hasOwn(instance, root)) return false;
825
- const descriptor = Object.getOwnPropertyDescriptor(instance, root);
826
- if (!descriptor.enumerable || descriptor.get !== void 0) return false;
827
- const cls = instance.constructor;
828
- if (lockedOf(cls).has(root) || computedOf(cls).has(root)) return false;
829
- let node = descriptor.value;
830
- for (const segment of segments.slice(1)) {
831
- if (isModel(node) || node instanceof Collection2) return false;
832
- if (Array.isArray(node) && !isArraySlot(node, segment)) return false;
833
- if (typeof node !== "object" || node === null || !Object.hasOwn(node, segment)) return true;
834
- node = node[segment];
835
- }
836
- return !isModel(node) && !(node instanceof Collection2);
837
- }
838
-
839
871
  // WithPagination.ts
840
872
  import { AsyncLocalStorage } from "async_hooks";
841
873
  var PAGINATED = /* @__PURE__ */ Symbol.for("oriel:live.pagination");