@protobase/schema 0.15.1 → 0.15.3

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.
@@ -13,4 +13,15 @@ export declare const fillAddress: (to: AppAddress, record?: Record<string, unkno
13
13
  export declare const leavesApp: (href: string) => boolean;
14
14
  /** Why an href cannot be a link's `href`: it points inside the app, which takes an address in `to`. */
15
15
  export declare const appHrefError: (href: string) => string | undefined;
16
+ /** The field an action's href is made of alone, `{field}`: the record's value is the whole URL, as it is stored. */
17
+ export declare const hrefField: (href: string) => string | undefined;
18
+ /**
19
+ * Why a link action cannot be built: an `href` that points inside the app (one that is a single `{field}`, the URL the
20
+ * record holds, is fine), or `{host}` in the label of a link that goes `to` an address in the app, which has no host.
21
+ */
22
+ export declare const linkActionError: (label: string, run: {
23
+ to: AppAddress;
24
+ } | {
25
+ href: string;
26
+ }) => string | undefined;
16
27
  //#endregion
@@ -39,5 +39,15 @@ const fillAddress = (to, record = {}) => {
39
39
  const leavesApp = (href) => /^([a-z][a-z0-9+.-]*:|\/\/)/i.test(href);
40
40
  /** Why an href cannot be a link's `href`: it points inside the app, which takes an address in `to`. */
41
41
  const appHrefError = (href) => leavesApp(href) ? void 0 : `href "${href}" points inside the app; give the link \`to\` instead, an address such as { kind: 'record', resource: 'orders', key: '{order_id}' }`;
42
+ /** The field an action's href is made of alone, `{field}`: the record's value is the whole URL, as it is stored. */
43
+ const hrefField = (href) => /^\{([A-Za-z_][A-Za-z0-9_]*)\}$/.exec(href)?.[1];
44
+ /**
45
+ * Why a link action cannot be built: an `href` that points inside the app (one that is a single `{field}`, the URL the
46
+ * record holds, is fine), or `{host}` in the label of a link that goes `to` an address in the app, which has no host.
47
+ */
48
+ const linkActionError = (label, run) => {
49
+ if ("href" in run) return hrefField(run.href) ? void 0 : appHrefError(run.href);
50
+ return label.includes("{host}") ? "the label names {host}, which only a link with an href has" : void 0;
51
+ };
42
52
  //#endregion
43
- export { addressPlaceholders, appHrefError, fillAddress, hrefPlaceholders, leavesApp };
53
+ export { addressPlaceholders, appHrefError, fillAddress, hrefField, hrefPlaceholders, leavesApp, linkActionError };
@@ -1,6 +1,9 @@
1
1
  import { z } from "zod";
2
2
  //#region src/fields/file.ts
3
- /** A file column holds `{provider}:{path}?name=…&size=…` as text; writes send a ticket from `:upload` instead. */
3
+ /**
4
+ * A file column holds `{provider}:{path}?name=…&size=…` as text, or an https URL with `.externalUrls()`; writes send a
5
+ * ticket from `:upload` instead.
6
+ */
4
7
  const file = {
5
8
  type: "file",
6
9
  constraints: [],
@@ -27,6 +27,12 @@ export declare class FileField<V> extends Field<V> {
27
27
  * format: 'webp' } }`. `{ original }` stores that image instead of the upload. Other files get none.
28
28
  */
29
29
  variants(variants: Record<string, ImageVariant>, options?: ImageVariantOptions): FileField<V>;
30
+ /**
31
+ * Also takes an https URL of a file elsewhere, such as `https://images.example.com/photos/42.jpg`, stored as it is:
32
+ * never uploaded, without variants, and drawn from that address. See
33
+ * [External URLs](website/src/content/docs/reference/files.mdx#external-urls).
34
+ */
35
+ externalUrls(): FileField<V>;
30
36
  /** The column is nullable: the field may hold no file. */
31
37
  optional(): FileField<V | null>;
32
38
  /** Never written through the API. */
@@ -49,6 +49,14 @@ var FileField = class FileField extends Field {
49
49
  ...options.original && { original: checkedVariant("The original", options.original) }
50
50
  });
51
51
  }
52
+ /**
53
+ * Also takes an https URL of a file elsewhere, such as `https://images.example.com/photos/42.jpg`, stored as it is:
54
+ * never uploaded, without variants, and drawn from that address. See
55
+ * [External URLs](website/src/content/docs/reference/files.mdx#external-urls).
56
+ */
57
+ externalUrls() {
58
+ return this.withFile({ external: true });
59
+ }
52
60
  /** The column is nullable: the field may hold no file. */
53
61
  optional() {
54
62
  return this.with({ nullable: true });
@@ -64,6 +64,8 @@ export type FileOptions = {
64
64
  variants?: Record<string, ImageVariant>;
65
65
  /** The image stored instead of the upload (`.variants()`' `original`). */
66
66
  original?: ImageVariant;
67
+ /** Whether a value may also be an https URL of a file elsewhere (`.externalUrls()`). */
68
+ external?: true;
67
69
  };
68
70
  /** Bytes from a number or text such as `10 MB` (decimal units) or `5 MiB` (binary units). */
69
71
  export declare const parseSize: (size: number | string) => number;
@@ -10,8 +10,8 @@ import { SearchTerm, matchedSearchField, matchesSearch, searchTerms, termMatches
10
10
  import { defaultRecentLimit, navRecentErrors, statusTones } from "./nav-recent.js";
11
11
  import { PluginHost, PluginRequirement, inRange, pluginProblem } from "./plugin-version.js";
12
12
  import { redactSecrets } from "./redact-secrets.js";
13
- import { addressPlaceholders, appHrefError, fillAddress, hrefPlaceholders, leavesApp } from "./address-template.js";
13
+ import { addressPlaceholders, appHrefError, fillAddress, hrefField, hrefPlaceholders, leavesApp, linkActionError } from "./address-template.js";
14
14
  import { AddressCheck, addressErrors } from "./address-errors.js";
15
15
  import { applyAssistantEvent, emptyAssistantState } from "./assistant/apply-event.js";
16
16
  import { readEventStream } from "./assistant/event-stream.js";
17
- export { type AddressCheck, type PluginHost, type PluginRequirement, type PropShapes, type RefShape, type SearchTerm, addressErrors, addressPlaceholders, appHrefError, applyAssistantEvent, bottomBarFor, cardPhotoLimit, checkEventFilter, checkedAccept, checkedProvider, decodeKey, defaultRecentLimit, displayFields, emptyAssistantState, encodeKey, fieldRefsIn, fillAddress, hrefPlaceholders, inRange, isFieldRef, leavesApp, markFieldRefs, matchedSearchField, matchesSearch, navRecentErrors, parseSize, pluginProblem, pruneByShape, readEventStream, redactSecrets, searchTerms, shapes, statusTones, tableDisplay, termMatches, unmarkFieldRefs, webhookResources };
17
+ export { type AddressCheck, type PluginHost, type PluginRequirement, type PropShapes, type RefShape, type SearchTerm, addressErrors, addressPlaceholders, appHrefError, applyAssistantEvent, bottomBarFor, cardPhotoLimit, checkEventFilter, checkedAccept, checkedProvider, decodeKey, defaultRecentLimit, displayFields, emptyAssistantState, encodeKey, fieldRefsIn, fillAddress, hrefField, hrefPlaceholders, inRange, isFieldRef, leavesApp, linkActionError, markFieldRefs, matchedSearchField, matchesSearch, navRecentErrors, parseSize, pluginProblem, pruneByShape, readEventStream, redactSecrets, searchTerms, shapes, statusTones, tableDisplay, termMatches, unmarkFieldRefs, webhookResources };
package/dist/internal.js CHANGED
@@ -2,7 +2,7 @@ import { checkedAccept, checkedProvider, parseSize } from "./files/file-options.
2
2
  import { matchedSearchField, matchesSearch, searchTerms, termMatches } from "./search-match.js";
3
3
  import { checkEventFilter } from "./events/event-models.js";
4
4
  import { defaultRecentLimit, navRecentErrors, statusTones } from "./nav-recent.js";
5
- import { addressPlaceholders, appHrefError, fillAddress, hrefPlaceholders, leavesApp } from "./address-template.js";
5
+ import { addressPlaceholders, appHrefError, fillAddress, hrefField, hrefPlaceholders, leavesApp, linkActionError } from "./address-template.js";
6
6
  import { fieldRefsIn, isFieldRef, markFieldRefs, unmarkFieldRefs } from "./field-ref.js";
7
7
  import { cardPhotoLimit, displayFields, tableDisplay } from "./view-displays.js";
8
8
  import { bottomBarFor } from "./bottom-bar.js";
@@ -14,4 +14,4 @@ import { redactSecrets } from "./redact-secrets.js";
14
14
  import { addressErrors } from "./address-errors.js";
15
15
  import { applyAssistantEvent, emptyAssistantState } from "./assistant/apply-event.js";
16
16
  import { readEventStream } from "./assistant/event-stream.js";
17
- export { addressErrors, addressPlaceholders, appHrefError, applyAssistantEvent, bottomBarFor, cardPhotoLimit, checkEventFilter, checkedAccept, checkedProvider, decodeKey, defaultRecentLimit, displayFields, emptyAssistantState, encodeKey, fieldRefsIn, fillAddress, hrefPlaceholders, inRange, isFieldRef, leavesApp, markFieldRefs, matchedSearchField, matchesSearch, navRecentErrors, parseSize, pluginProblem, pruneByShape, readEventStream, redactSecrets, searchTerms, shapes, statusTones, tableDisplay, termMatches, unmarkFieldRefs, webhookResources };
17
+ export { addressErrors, addressPlaceholders, appHrefError, applyAssistantEvent, bottomBarFor, cardPhotoLimit, checkEventFilter, checkedAccept, checkedProvider, decodeKey, defaultRecentLimit, displayFields, emptyAssistantState, encodeKey, fieldRefsIn, fillAddress, hrefField, hrefPlaceholders, inRange, isFieldRef, leavesApp, linkActionError, markFieldRefs, matchedSearchField, matchesSearch, navRecentErrors, parseSize, pluginProblem, pruneByShape, readEventStream, redactSecrets, searchTerms, shapes, statusTones, tableDisplay, termMatches, unmarkFieldRefs, webhookResources };
package/dist/model.d.ts CHANGED
@@ -61,6 +61,8 @@ export type FileFieldModel = {
61
61
  variants?: Record<string, ImageVariant>;
62
62
  /** What is stored instead of an uploaded image, when the upload is not kept. */
63
63
  original?: ImageVariant;
64
+ /** Whether a value may also be an https URL of a file elsewhere, drawn and linked as it is (`.externalUrls()`). */
65
+ external?: true;
64
66
  };
65
67
  /** `digitsEnd`: only the end of the value's digits, spaces, `+` and dashes ignored (phone numbers). */
66
68
  export type SearchMatch = 'digitsEnd';
@@ -583,9 +585,12 @@ export type ActionRun = {
583
585
  /** The address, with `{field}` replaced by the record's values. */
584
586
  to: AppAddress;
585
587
  } | {
586
- /** Opens a link that leaves the app. */
588
+ /** Opens a link that leaves the app, an `https:` or `http:` one in a new tab without the referrer. */
587
589
  kind: 'link';
588
- /** The link (`https:`, `mailto:`), with `{field}` replaced by the record's values, percent-encoded. */
590
+ /**
591
+ * The link (`https:`, `mailto:`), with `{field}` replaced by the record's values, percent-encoded; or only `{field}`,
592
+ * the `https:` or `http:` URL the record holds, without which the action does not show.
593
+ */
589
594
  href: string;
590
595
  };
591
596
  /** A named action of a view (`.actions((a) => ...)`); see [Named actions](website/src/content/docs/reference/layouts.md#named-actions). */
@@ -594,7 +599,7 @@ export type ActionModel = {
594
599
  name: string;
595
600
  /** The button's label. */
596
601
  label: string;
597
- /** The button's icon: `bed`, `file-text`, `ruler`, `send` or `wallet`; another name shows a lightning bolt. */
602
+ /** The button's icon: `bed`, `bell`, `bookmark`, `external-link`, `file-text`, `heart`, `ruler`, `send` or `wallet`; another name shows a lightning bolt. */
598
603
  icon?: string;
599
604
  /** The question the button asks before it runs the action. */
600
605
  confirm?: string;
@@ -38,7 +38,8 @@ const fieldModel = (name, field) => {
38
38
  ...writtenFields(meta.file).length > 0 && { derive: writtenFields(meta.file) },
39
39
  ...meta.file.placeholder !== void 0 && { placeholder: meta.file.placeholder },
40
40
  ...meta.file.variants && Object.keys(meta.file.variants).length > 0 && { variants: structuredClone(meta.file.variants) },
41
- ...meta.file.original && { original: { ...meta.file.original } }
41
+ ...meta.file.original && { original: { ...meta.file.original } },
42
+ ...meta.file.external && { external: true }
42
43
  } }
43
44
  };
44
45
  };
@@ -19,8 +19,10 @@ export declare const actions: {
19
19
  remove: (name: string, o: ActionOptions) => ActionModel;
20
20
  /**
21
21
  * Goes `to` an address in the app, such as `{ kind: 'record', resource: 'orders', key: '{order_id}' }`, or opens an
22
- * `href` that leaves it (`https:`, `mailto:`); `{field}` is replaced by the record's value. A path in the app is
23
- * refused as an `href`, so every address in the app is written one way.
22
+ * `href` that leaves it (`https:`, `mailto:`); `{field}` is replaced by the record's value. An `https:` or `http:` link
23
+ * opens in a new tab without the referrer. An href that is only `{field}`, such as `'{website}'`, opens the URL the
24
+ * record holds, and the button shows only when it is an `https:` or `http:` URL; `{host}` in the label becomes its host,
25
+ * as in `'View on {host}'`. A path in the app is refused as an `href`, so every address in the app is written one way.
24
26
  */
25
27
  link: (name: string, o: Omit<ActionOptions, "confirm"> & ({
26
28
  to: AppAddress;
@@ -1,4 +1,4 @@
1
- import { appHrefError } from "./address-template.js";
1
+ import { linkActionError } from "./address-template.js";
2
2
  //#region src/view-actions.ts
3
3
  const named = (name, o) => ({
4
4
  name,
@@ -25,10 +25,14 @@ const actions = {
25
25
  }),
26
26
  /**
27
27
  * Goes `to` an address in the app, such as `{ kind: 'record', resource: 'orders', key: '{order_id}' }`, or opens an
28
- * `href` that leaves it (`https:`, `mailto:`); `{field}` is replaced by the record's value. A path in the app is
29
- * refused as an `href`, so every address in the app is written one way.
28
+ * `href` that leaves it (`https:`, `mailto:`); `{field}` is replaced by the record's value. An `https:` or `http:` link
29
+ * opens in a new tab without the referrer. An href that is only `{field}`, such as `'{website}'`, opens the URL the
30
+ * record holds, and the button shows only when it is an `https:` or `http:` URL; `{host}` in the label becomes its host,
31
+ * as in `'View on {host}'`. A path in the app is refused as an `href`, so every address in the app is written one way.
30
32
  */
31
33
  link: (name, o) => {
34
+ const error = linkActionError(o.label, o);
35
+ if (error) throw new Error(`Action "${name}": ${error}`);
32
36
  if ("to" in o) return {
33
37
  ...named(name, o),
34
38
  run: {
@@ -36,8 +40,6 @@ const actions = {
36
40
  to: structuredClone(o.to)
37
41
  }
38
42
  };
39
- const error = appHrefError(o.href);
40
- if (error) throw new Error(`Action "${name}": ${error}`);
41
43
  return {
42
44
  ...named(name, o),
43
45
  run: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/schema",
3
- "version": "0.15.1",
3
+ "version": "0.15.3",
4
4
  "description": "Resource, field, view and access definitions for Protobase configs.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {