@lowdefy/engine 5.5.0 → 5.6.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/dist/Block.js CHANGED
@@ -247,6 +247,9 @@ let Block = class Block {
247
247
  if (beforeVisible !== this.visibleEval.output) {
248
248
  repeat.value = true;
249
249
  }
250
+ if (this.isList() && !this.isVisible()) {
251
+ this.captureHiddenValue();
252
+ }
250
253
  if (this.visibleEval.output !== false) {
251
254
  this.propertiesEval = this.parse(this.properties);
252
255
  this.requiredEval = this.parse(this.required);
@@ -347,8 +350,23 @@ let Block = class Block {
347
350
  visibleEval: this.visibleEval
348
351
  });
349
352
  });
353
+ _define_property(this, "captureHiddenValue", ()=>{
354
+ const stateValue = get(this.context.state, this.blockId);
355
+ if (type.isUndefined(stateValue)) return;
356
+ this.hiddenValue = serializer.copy(stateValue);
357
+ });
358
+ _define_property(this, "restoreHiddenValue", ()=>{
359
+ if (type.isUndefined(this.hiddenValue)) return;
360
+ if (type.isUndefined(get(this.context.state, this.blockId))) {
361
+ this.context._internal.State.set(this.blockId, this.hiddenValue);
362
+ }
363
+ this.hiddenValue = undefined;
364
+ });
350
365
  _define_property(this, "updateState", (toSet)=>{
351
366
  if (!this.isVisible()) return;
367
+ if (this.isList()) {
368
+ this.restoreHiddenValue();
369
+ }
352
370
  if (this.isContainer() || this.isList()) {
353
371
  if (this.subSlots && this.subSlots.length > 0) {
354
372
  this.loopSubSlots((subSlotsClass)=>subSlotsClass.updateState());
package/dist/State.js CHANGED
@@ -12,7 +12,7 @@
12
12
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
- */ import { unset, get, serializer, set, swap, type } from '@lowdefy/helpers';
15
+ */ import { unset, get, joinPath, serializer, set, splitPath, swap, type } from '@lowdefy/helpers';
16
16
  let State = class State {
17
17
  resetState() {
18
18
  Object.keys(this.context.state).forEach((key)=>{
@@ -35,9 +35,9 @@ let State = class State {
35
35
  del(field) {
36
36
  unset(this.context.state, field);
37
37
  // remove all empty objects from state as an effect of deleted values
38
- const fields = field.split('.');
38
+ const fields = splitPath(field);
39
39
  if (fields.length > 1) {
40
- const parent = fields.slice(0, fields.length - 1).join('.');
40
+ const parent = joinPath(fields.slice(0, -1));
41
41
  const parentValue = get(this.context.state, parent);
42
42
  if (type.isObject(parentValue) && Object.keys(parentValue).length === 0) {
43
43
  this.del(parent);
@@ -12,7 +12,7 @@
12
12
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
- */ import { applyArrayIndices, get, serializer, type } from '@lowdefy/helpers';
15
+ */ import { ReservedKeyError, applyArrayIndices, get, serializer, type } from '@lowdefy/helpers';
16
16
  const getFromObject = ({ arrayIndices, location, method, object, params })=>{
17
17
  if (params === true) params = {
18
18
  all: true
@@ -23,20 +23,26 @@ const getFromObject = ({ arrayIndices, location, method, object, params })=>{
23
23
  if (!type.isObject(params)) {
24
24
  throw new Error(`Method Error: ${method} params must be of type string, integer, boolean or object at ${location}.`);
25
25
  }
26
- if (params.key === null) return get(params, 'default', {
26
+ const defaultValue = get(params, 'default', {
27
27
  default: null,
28
28
  copy: true
29
29
  });
30
+ if (params.key === null) return defaultValue;
30
31
  if (params.all === true) return serializer.copy(object);
31
32
  if (!type.isString(params.key) && !type.isInt(params.key)) {
32
33
  throw new Error(`Method Error: ${method} params.key must be of type string or integer at ${location}.`);
33
34
  }
34
- return get(object, applyArrayIndices(arrayIndices, params.key), {
35
- default: get(params, 'default', {
36
- default: null,
35
+ try {
36
+ return get(object, applyArrayIndices(arrayIndices, params.key), {
37
+ default: defaultValue,
37
38
  copy: true
38
- }),
39
- copy: true
40
- });
39
+ });
40
+ } catch (error) {
41
+ // Tier 2: a runtime read of an app-developer keypath. There is no loud failure that helps here —
42
+ // a thrown error in the browser is worse than the default, and the reserved rule's job (refusing
43
+ // the read) is already done. Author-written identifiers fail loudly at build instead.
44
+ if (error instanceof ReservedKeyError) return defaultValue;
45
+ throw error;
46
+ }
41
47
  };
42
48
  export default getFromObject;
@@ -12,60 +12,68 @@
12
12
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
- */ import { type, urlQuery as urlQueryFn } from '@lowdefy/helpers';
15
+ */ import { type } from '@lowdefy/helpers';
16
+ import resolveTarget from './resolveTarget.js';
16
17
  function createLink({ backLink, disabledLink, lowdefy, newOriginLink, noLink, sameOriginLink }) {
17
18
  function link(props) {
18
19
  if (props.disabled === true) {
19
20
  return disabledLink(props);
20
21
  }
21
- if ([
22
- !props.pageId,
23
- !props.back,
24
- !props.home,
25
- !props.href,
26
- !props.url
27
- ].filter((v)=>!v).length > 1) {
28
- throw new Error(`Invalid Link: To avoid ambiguity, only one of 'back', 'home', 'href', 'pageId' or 'url' can be defined.`);
29
- }
22
+ // back has no pathname to resolve and cannot carry input or urlQuery.
30
23
  if (props.back === true) {
31
- // Cannot set input or urlQuery on back
32
24
  return backLink(props);
33
25
  }
34
- const query = type.isNone(props.urlQuery) ? '' : `${urlQueryFn.stringify(props.urlQuery)}`;
35
- if (props.home === true) {
36
- const pathname = `/${lowdefy.home.configured ? '' : lowdefy.home.pageId}`;
37
- return sameOriginLink({
38
- ...props,
39
- pathname,
40
- query,
41
- setInput: ()=>{
42
- lowdefy.inputs[`page:${lowdefy.home.pageId}`] = props.input ?? {};
43
- }
44
- });
45
- }
46
- if (type.isString(props.pageId)) {
47
- return sameOriginLink({
48
- ...props,
49
- pathname: `/${props.pageId}`,
50
- query,
51
- setInput: ()=>{
52
- lowdefy.inputs[`page:${props.pageId}`] = props.input ?? {};
53
- }
54
- });
55
- }
26
+ // href is an HTML-attribute passthrough the <Link> component reads, not a
27
+ // navigation target, so it never enters the grammar resolver.
56
28
  if (type.isString(props.href)) {
57
29
  return newOriginLink(props);
58
30
  }
59
- if (type.isString(props.url)) {
60
- const protocol = props.url.includes(':') ? '' : 'https://';
31
+ const target = resolveTarget({
32
+ lowdefy,
33
+ target: {
34
+ home: props.home,
35
+ pageId: props.pageId,
36
+ url: props.url,
37
+ urlQuery: props.urlQuery
38
+ }
39
+ });
40
+ if (type.isNone(target)) {
41
+ return noLink(props);
42
+ }
43
+ if (target.kind === 'external') {
44
+ // The resolver's href is the whole URL with any query already folded in,
45
+ // so it is passed as the url prop the callback reads with an empty query.
61
46
  return newOriginLink({
62
47
  ...props,
63
- url: `${protocol}${props.url}`,
64
- query
48
+ url: target.href,
49
+ query: ''
65
50
  });
66
51
  }
67
- return noLink(props);
52
+ return sameOriginLink({
53
+ ...props,
54
+ pathname: target.pathname,
55
+ query: target.query,
56
+ setInput: getSetInput({
57
+ lowdefy,
58
+ props
59
+ })
60
+ });
68
61
  }
69
62
  return link;
70
63
  }
64
+ // A page-kind url names no page, so it seeds no input - writing
65
+ // inputs['page:undefined'] is the bug family a no-op setInput avoids.
66
+ function getSetInput({ lowdefy, props }) {
67
+ if (props.home === true) {
68
+ return ()=>{
69
+ lowdefy.inputs[`page:${lowdefy.home.pageId}`] = props.input ?? {};
70
+ };
71
+ }
72
+ if (type.isString(props.pageId)) {
73
+ return ()=>{
74
+ lowdefy.inputs[`page:${props.pageId}`] = props.input ?? {};
75
+ };
76
+ }
77
+ return ()=>{};
78
+ }
71
79
  export default createLink;
@@ -0,0 +1,34 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import { type } from '@lowdefy/helpers';
16
+ // The single reading of `lowdefy.home` for every 'home' target - the Link
17
+ // grammar in createLink and the post-auth callbackUrl ladder in the client both
18
+ // resolve through here. Returns undefined when the app names no home page:
19
+ // getHomeAndMenus resolves pageId to null for an app with no homePageId whose
20
+ // authorized menu yields no link, and each caller decides what no-home means
21
+ // for it (an invalid link, no callback target) rather than interpolating the
22
+ // missing value into a path.
23
+ function getHomePathname({ lowdefy }) {
24
+ // A configured homePageId is served at the app root - the server resolves `/`
25
+ // to that page, so the pageId is deliberately not in the path.
26
+ if (lowdefy.home?.configured === true) {
27
+ return '/';
28
+ }
29
+ if (type.isString(lowdefy.home?.pageId)) {
30
+ return `/${lowdefy.home.pageId}`;
31
+ }
32
+ return undefined;
33
+ }
34
+ export default getHomePathname;
package/dist/index.js CHANGED
@@ -17,7 +17,9 @@ import Slots from './Slots.js';
17
17
  import createLink from './createLink.js';
18
18
  import Events from './Events.js';
19
19
  import getContext from './getContext.js';
20
+ import getHomePathname from './getHomePathname.js';
20
21
  import Requests from './Requests.js';
22
+ import resolveTarget from './resolveTarget.js';
21
23
  import State from './State.js';
22
- export { Actions, Slots, createLink, Events, Requests, State };
24
+ export { Actions, Slots, createLink, Events, getHomePathname, Requests, resolveTarget, State };
23
25
  export default getContext;
@@ -0,0 +1,126 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import { type, urlQuery as urlQueryFn } from '@lowdefy/helpers';
16
+ import getHomePathname from './getHomePathname.js';
17
+ // Classifies a `url` grammar value into a page or external target. basePath is
18
+ // stripped here, never applied - the single application boundary is createUrl.
19
+ function classifyUrl({ lowdefy, url, query }) {
20
+ // The leading-slash test runs before the colon-less test: `/2fa` is an
21
+ // app-relative page and colon-less, and the colon-less branch would wrongly
22
+ // give it an `https://` scheme and parse it as an off-app origin.
23
+ if (url.startsWith('/')) {
24
+ const questionMark = url.indexOf('?');
25
+ const pathname = questionMark === -1 ? url : url.slice(0, questionMark);
26
+ const ownQuery = questionMark === -1 ? '' : url.slice(questionMark + 1);
27
+ // The target's own urlQuery combines with any query the string carries,
28
+ // matching the grammar semantics createLink resolves today.
29
+ const combined = [
30
+ ownQuery,
31
+ query
32
+ ].filter((part)=>part !== '').join('&');
33
+ return {
34
+ kind: 'page',
35
+ pathname,
36
+ query: combined
37
+ };
38
+ }
39
+ // A colon-less value like `example.com` is a schemeless hostname, not a path -
40
+ // prepend `https://` so the URL parser reads it as an absolute URL rather than
41
+ // the app-relative path `/example.com`. Confined to here by the leading-slash
42
+ // test above, so a colon-bearing path like `/path:1` never reaches it.
43
+ const value = url.includes(':') ? url : `https://${url}`;
44
+ const origin = lowdefy._internal?.globals?.window?.location?.origin;
45
+ // No window (SSR, tests): a `url` that reaches origin classification cannot be
46
+ // placed, so it resolves to nothing rather than dereferencing a missing window.
47
+ if (type.isNone(origin)) {
48
+ return undefined;
49
+ }
50
+ const parsed = new URL(value, origin);
51
+ const basePath = lowdefy.basePath ?? '';
52
+ if (parsed.origin === origin) {
53
+ const insideBasePath = basePath === '' || parsed.pathname.startsWith(basePath);
54
+ if (insideBasePath) {
55
+ // Strip basePath so the router does not re-apply it: an absolute
56
+ // `https://myapp.com/app/reports` under basePath `/app` already carries the
57
+ // prefix, and without stripping router.push would push `/app/app/reports`.
58
+ const pathname = parsed.pathname.startsWith(basePath) ? parsed.pathname.slice(basePath.length) : parsed.pathname;
59
+ return {
60
+ kind: 'page',
61
+ pathname,
62
+ query: parsed.search.replace(/^\?/, '')
63
+ };
64
+ }
65
+ // Same origin but outside basePath (a marketing page at the origin root while
66
+ // the app lives at `/app`) is a whole URL - routing it would 404 in `/app`.
67
+ return {
68
+ kind: 'external',
69
+ href: parsed.href
70
+ };
71
+ }
72
+ return {
73
+ kind: 'external',
74
+ href: parsed.href
75
+ };
76
+ }
77
+ // The single resolver of the navigation grammar { home, pageId, url, urlQuery }
78
+ // for every reader. Returns a discriminated, un-prefixed target - never a string,
79
+ // never basePath-prefixed - so the page/external distinction is data the consumer
80
+ // reads rather than a shape it guesses from a leading slash.
81
+ function resolveTarget({ lowdefy, target, name = 'Link' }) {
82
+ if (!type.isObject(target)) {
83
+ return undefined;
84
+ }
85
+ const { home, pageId, url, urlQuery } = target;
86
+ const defined = [
87
+ home,
88
+ pageId,
89
+ url
90
+ ].filter((value)=>value);
91
+ if (defined.length > 1) {
92
+ throw new Error(`Invalid ${name}: To avoid ambiguity, only one of 'home', 'pageId' or 'url' can be defined.`);
93
+ }
94
+ const query = type.isNone(urlQuery) ? '' : `${urlQueryFn.stringify(urlQuery)}`;
95
+ if (home === true) {
96
+ const pathname = getHomePathname({
97
+ lowdefy
98
+ });
99
+ // An app whose home config names no page has no resolvable home - propagate
100
+ // getHomePathname's undefined rather than building the literal "/undefined".
101
+ if (type.isNone(pathname)) {
102
+ return undefined;
103
+ }
104
+ return {
105
+ kind: 'page',
106
+ pathname,
107
+ query
108
+ };
109
+ }
110
+ if (type.isString(pageId)) {
111
+ return {
112
+ kind: 'page',
113
+ pathname: `/${pageId}`,
114
+ query
115
+ };
116
+ }
117
+ if (type.isString(url)) {
118
+ return classifyUrl({
119
+ lowdefy,
120
+ url,
121
+ query
122
+ });
123
+ }
124
+ return undefined;
125
+ }
126
+ export default resolveTarget;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lowdefy/engine",
3
- "version": "5.5.0",
3
+ "version": "5.6.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "",
6
6
  "homepage": "https://lowdefy.com",
@@ -30,16 +30,16 @@
30
30
  "dist/*"
31
31
  ],
32
32
  "dependencies": {
33
- "@lowdefy/errors": "5.5.0",
34
- "@lowdefy/helpers": "5.5.0",
35
- "@lowdefy/operators": "5.5.0"
33
+ "@lowdefy/errors": "5.6.0",
34
+ "@lowdefy/helpers": "5.6.0",
35
+ "@lowdefy/operators": "5.6.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@jest/globals": "28.1.3",
39
- "@lowdefy/actions-core": "5.5.0",
40
- "@lowdefy/build": "5.5.0",
41
- "@lowdefy/operators-js": "5.5.0",
42
- "@lowdefy/operators-mql": "5.5.0",
39
+ "@lowdefy/actions-core": "5.6.0",
40
+ "@lowdefy/build": "5.6.0",
41
+ "@lowdefy/operators-js": "5.6.0",
42
+ "@lowdefy/operators-mql": "5.6.0",
43
43
  "@swc/cli": "0.8.0",
44
44
  "@swc/core": "1.15.18",
45
45
  "@swc/jest": "0.2.39",