@valbuild/core 0.121.0 → 0.124.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/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # @valbuild/core
2
2
 
3
+ ## 0.124.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#622](https://github.com/valbuild/val/pull/622) [`aa89fc2`](https://github.com/valbuild/val/commit/aa89fc26de7028b3c5a1666b99afc03b39e92769) Thanks [@freekh](https://github.com/freekh)! - **Breaking:** `s.images()` and `s.files()` no longer take a `remote` option. Use
8
+ `.remote()`, the same way `s.image()` and `s.file()` already did.
9
+
10
+ ```ts
11
+ // before
12
+ s.images({ directory: "/public/val/images", remote: true });
13
+ s.files({
14
+ directory: "/public/val/docs",
15
+ accept: "application/pdf",
16
+ remote: true,
17
+ });
18
+
19
+ // after
20
+ s.images({ directory: "/public/val/images" }).remote();
21
+ s.files({
22
+ directory: "/public/val/docs",
23
+ accept: "application/pdf",
24
+ }).remote();
25
+ ```
26
+
27
+ Behaviour is unchanged — remote is still off unless asked for. This only makes
28
+ remote one thing spelled one way across all four media schemas, instead of an
29
+ option on the two collections and a method on the two fields. `remote: false`
30
+ has no replacement because it was the default; drop it.
31
+
3
32
  ## 0.121.0
4
33
 
5
34
  ### Minor Changes
@@ -3,13 +3,14 @@ export { modules, type ValModules } from "./modules.js";
3
3
  export { extractValModules, computeValModuleShas, type ExtractedValModules, type ValModuleShaEntry, type ValModuleShas, type ExtractedModuleError, } from "./extractValModules.js";
4
4
  export type { InitVal, ValConfig, ValConstructor, ContentConstructor, } from "./initVal.js";
5
5
  export { Schema, type SerializedSchema, type SelectorOfSchema } from "./schema/index.js";
6
- export { hasRemoteFileSchema } from "./schema/hasRemoteFileSchema.js";
6
+ export { hasRemoteFileSchema, hasMediaSchema, } from "./schema/hasRemoteFileSchema.js";
7
7
  export type { ImageMetadata, ImageEncodeOption, ImageEncodeOptions, } from "./schema/image.js";
8
8
  export type { FileMetadata } from "./schema/file.js";
9
9
  export type { ValModule, SerializedModule, InferValModuleType } from "./module.js";
10
10
  export type { SourceObject, SourcePrimitive, Source } from "./source/index.js";
11
11
  export type { FileSource } from "./source/media.js";
12
12
  export type { JsonSource, JsonOf, JsonImportThunk } from "./source/json.js";
13
+ export type { ExternalRecordSrc, ExternalRecordWritableSrc, ExternalItemOf, ExternalLabelOf, ExternalReadonlyOf, } from "./source/external.js";
13
14
  export type { RemoteRef } from "./source/remote.js";
14
15
  export { DEFAULT_VAL_REMOTE_HOST } from "./schema/remote.js";
15
16
  export type { RawString } from "./schema/string.js";
@@ -35,6 +36,8 @@ import { getMimeType, mimeTypeToFileExt, filenameToMimeType, mimeTypeMatchesAcce
35
36
  import { type ImageMetadata } from "./schema/image.js";
36
37
  import { type FileMetadata } from "./schema/file.js";
37
38
  import { isJson, getJsonImport, resolveJsonValues } from "./source/json.js";
39
+ import { isExternal, hasInlineExternalEntries } from "./source/external.js";
40
+ import { createExternalFileRef, splitExternalFileRef, isExternalFileRef } from "./source/externalRef.js";
38
41
  import { createRemoteRef } from "./source/remote.js";
39
42
  import { getValidationBasis, getValidationHash } from "./remote/validationBasis.js";
40
43
  import { getFileHash, hashToRemoteFileHash } from "./remote/fileHash.js";
@@ -111,6 +114,11 @@ declare const Internal: {
111
114
  isJson: typeof isJson;
112
115
  getJsonImport: typeof getJsonImport;
113
116
  resolveJsonValues: typeof resolveJsonValues;
117
+ isExternal: typeof isExternal;
118
+ hasInlineExternalEntries: typeof hasInlineExternalEntries;
119
+ createExternalFileRef: typeof createExternalFileRef;
120
+ splitExternalFileRef: typeof splitExternalFileRef;
121
+ isExternalFileRef: typeof isExternalFileRef;
114
122
  createValPathOfItem: typeof createValPathOfItem;
115
123
  getSHA256Hash: (bits: Uint8Array) => string;
116
124
  initSchema: typeof initSchema;
@@ -2,9 +2,11 @@ import { define } from "./module.js";
2
2
  import { InitSchema } from "./initSchema.js";
3
3
  import { getValPath as getPath } from "./val/index.js";
4
4
  import { json } from "./source/json.js";
5
+ import { external } from "./source/external.js";
5
6
  export type ContentConstructor = {
6
7
  define: typeof define;
7
8
  json: typeof json;
9
+ external: typeof external;
8
10
  };
9
11
  export type ValConstructor = {
10
12
  unstable_getPath: typeof getPath;
@@ -1,6 +1,7 @@
1
1
  import { Schema, SelectorOfSchema, SerializedSchema } from "./schema/index.js";
2
2
  import { GenericSelector, SelectorOf, SelectorSource } from "./selector/index.js";
3
3
  import { Source } from "./source/index.js";
4
+ import { ExternalRecordSrc } from "./source/external.js";
4
5
  import { ModuleFilePath, ModulePath, SourcePath } from "./val/index.js";
5
6
  import { Json } from "./Json.js";
6
7
  import { RawString } from "./schema/string.js";
@@ -15,8 +16,24 @@ export type ReplaceRawStringWithString<T extends SelectorSource> = SelectorSourc
15
16
  } ? {
16
17
  [key in keyof T]: ReplaceRawStringWithString<T[key]>;
17
18
  } : T extends SelectorSource[] ? ReplaceRawStringWithString<T[number]>[] : T;
19
+ /**
20
+ * What may be WRITTEN as an external record's source, on top of the marker.
21
+ *
22
+ * Entries inline in the `.val.ts` are accepted on purpose — the same call
23
+ * `.jsonValues()` makes for the same reason: pasting content straight into the
24
+ * module is the natural first thing to write, and a type error there is a dead
25
+ * end, because the author cannot see what to write instead. Validation reports
26
+ * them as an `external:upload` fix, and they are readable at runtime meanwhile.
27
+ *
28
+ * Widened HERE rather than in the schema's own source type so that the MODULE
29
+ * stays typed on the marker alone. An adapter is bound to a module, and a module
30
+ * whose type were "marker or plain record" would be indistinguishable from an
31
+ * ordinary record — which would let `entry()` accept a module that is not
32
+ * external at all.
33
+ */
34
+ type InlineEntriesFor<S> = S extends ExternalRecordSrc<infer Item, string, boolean> ? Record<string, Item> : never;
18
35
  export declare function define<T extends Schema<SelectorSource>>(id: string, // TODO: `/${string}`
19
- schema: T, source: ReplaceRawStringWithString<SelectorOfSchema<T>>): ValModule<SelectorOfSchema<T>>;
36
+ schema: T, source: ReplaceRawStringWithString<SelectorOfSchema<T>> | InlineEntriesFor<SelectorOfSchema<T>>): ValModule<SelectorOfSchema<T>>;
20
37
  export declare function getSource<T extends Source>(valModule: ValModule<T>): T;
21
38
  /**
22
39
  * Whether `value` is a Val module, i.e. something {@link define} produced.
@@ -21,11 +21,6 @@ export type FilesOptions = {
21
21
  * a directory with every other one.
22
22
  */
23
23
  directory: "/public" | `/public/${string}`;
24
- /**
25
- * Whether remote files are allowed
26
- * @default false
27
- */
28
- remote?: boolean;
29
24
  };
30
25
  /**
31
26
  * Metadata for a file entry in the files record
@@ -43,6 +38,8 @@ type FilesItemSrc = {
43
38
  /**
44
39
  * Define a collection of files.
45
40
  *
41
+ * Remote is off by default: call `.remote()` on the result to allow remote files.
42
+ *
46
43
  * @example
47
44
  * ```typescript
48
45
  * const schema = s.files({
@@ -5,9 +5,10 @@ import type { SerializedSchema } from "./index.js";
5
5
  * The one answer to that question. There used to be two — `hasRemoteFileSchema`
6
6
  * in the server, gating whether `/save` demands remote credentials, and
7
7
  * `findRequiredRemoteFiles` in the Studio, gating the `/remote/settings` fetch —
8
- * and they disagreed about `s.images({ remote: true })`: the Studio's counted a
9
- * remote media record, the server's did not, because a media collection
10
- * serializes as a `record` of metadata rather than as an image schema.
8
+ * and they disagreed about `s.images({ ... }).remote()`: the Studio's version
9
+ * counted a remote media record, the server's did not, because a media
10
+ * collection serializes as a `record` of metadata rather than as an image
11
+ * schema.
11
12
  *
12
13
  * The server's answer was the wrong one, and silently so. With it false,
13
14
  * `/save` runs `saveOrUploadFiles` in `skip-remote` mode, and that mode does not
@@ -32,3 +33,20 @@ import type { SerializedSchema } from "./index.js";
32
33
  * instead would reintroduce exactly the silent skip described above.
33
34
  */
34
35
  export declare function hasRemoteFileSchema(schema: SerializedSchema): boolean;
36
+ /**
37
+ * Does this schema hold any media at all — remote or not?
38
+ *
39
+ * The sibling of {@link hasRemoteFileSchema}, and the same walk minus the
40
+ * `remote` predicate. It answers the question an external record has to ask
41
+ * before it will accept a binding: an item schema containing media needs a
42
+ * `putFile`/`getFile` pair, and without one that is a schema error.
43
+ *
44
+ * It is a walk over the SERIALIZED schema for the reasons given above, and for
45
+ * one more that is specific to this question: the two cases most likely to be
46
+ * missed are invisible to a value type. A media collection (`s.images()` /
47
+ * `s.files()`) serializes as a record of metadata with no image schema inside
48
+ * it, and a richtext inline image lives in a constructor argument. A check
49
+ * written against the item TYPE would silently pass both — and the gallery case
50
+ * already has a data-loss incident attached to it (see above).
51
+ */
52
+ export declare function hasMediaSchema(schema: SerializedSchema): boolean;
@@ -35,11 +35,6 @@ export type ImagesOptions<Accept extends `image/${string}`> = {
35
35
  * - s.record(s.string(), s.string()) for locale-based alt text
36
36
  */
37
37
  alt?: AltSchema;
38
- /**
39
- * Whether remote images are allowed
40
- * @default false
41
- */
42
- remote?: boolean;
43
38
  /**
44
39
  * Re-encode uploads in the browser before they are uploaded.
45
40
  *
@@ -79,7 +74,7 @@ type ImagesItemSrc = {
79
74
  *
80
75
  * `directory` is required — it decides where uploads land, so it is not something
81
76
  * to be inferred. The rest defaults: any image type (`"image/*"`), nullable alt
82
- * text, remote disabled.
77
+ * text, remote disabled. Call `.remote()` on the result to allow remote images.
83
78
  *
84
79
  * @example
85
80
  * ```typescript
@@ -4,6 +4,7 @@ import { FieldRender } from "../render.js";
4
4
  import { ValRouter } from "../router.js";
5
5
  import { SelectorSource } from "../selector/index.js";
6
6
  import { JsonOf, JsonSource } from "../source/json.js";
7
+ import { ExternalRecordSrc } from "../source/external.js";
7
8
  import { ModuleFilePath, SourcePath } from "../val/index.js";
8
9
  import { ValidationError, ValidationErrors } from "./validation/ValidationError.js";
9
10
  import type { ImageEncodeOption } from "./image.js";
@@ -38,6 +39,12 @@ export type SerializedRecordSchema = {
38
39
  encode?: ImageEncodeOption;
39
40
  alt?: SerializedSchema;
40
41
  jsonValues?: boolean;
42
+ /**
43
+ * The label from `.external(label)`. Its presence is what marks the record as
44
+ * externally stored; the value names the binding a reader — or a model — can
45
+ * grep for. See `source/external.ts`.
46
+ */
47
+ external?: string;
41
48
  readonly?: boolean;
42
49
  hidden?: boolean;
43
50
  description?: string;
@@ -56,7 +63,13 @@ export type SerializedRecordSchema = {
56
63
  * authoring.
57
64
  */
58
65
  export type JsonValuesRecordSrc<T extends Schema<SelectorSource>, K extends Schema<string>> = Record<SelectorOfSchema<K>, JsonSource<JsonOf<SelectorOfSchema<T>>> | SelectorOfSchema<T>>;
59
- export declare class RecordSchema<T extends Schema<SelectorSource>, K extends Schema<string>, Src extends Record<SelectorOfSchema<K>, SelectorOfSchema<T>> | JsonValuesRecordSrc<T, K> | null> extends Schema<Src> {
66
+ export declare class RecordSchema<T extends Schema<SelectorSource>, K extends Schema<string>, Src extends Record<SelectorOfSchema<K>, SelectorOfSchema<T>> | JsonValuesRecordSrc<T, K> | ExternalRecordSrc | null,
67
+ /**
68
+ * Whether `.readonly()` was called. It rides in the type — not only in
69
+ * `isReadonly` — because `.external()` freezes it into the source marker, and
70
+ * the adapter's write methods are required or forbidden on the strength of it.
71
+ */
72
+ RO extends boolean = false> extends Schema<Src> {
60
73
  private readonly item;
61
74
  private readonly opt;
62
75
  private readonly customValidateFunctions;
@@ -70,9 +83,31 @@ export declare class RecordSchema<T extends Schema<SelectorSource>, K extends Sc
70
83
  private readonly isJsonValues;
71
84
  private readonly previewInput;
72
85
  private readonly renderInput;
86
+ /**
87
+ * Set by `.external(label)`; the entries live behind that adapter.
88
+ *
89
+ * LAST on purpose: every other `new RecordSchema(...,
90
+ this.externalLabel,
91
+ )` in the codebase
92
+ * passes its arguments positionally, so a new parameter anywhere else
93
+ * silently shifts `previewInput` and `renderInput` at each of those call
94
+ * sites.
95
+ */
96
+ private readonly externalLabel;
73
97
  constructor(item: T, opt?: boolean, customValidateFunctions?: CustomValidateFunction<Src>[], currentRouter?: ValRouter | null, keySchema?: Schema<string> | null, mediaOptions?: MediaOptions | undefined, isReadonly?: boolean, isHidden?: boolean, description?: string | undefined,
74
98
  /** When true, entry values are lazily loaded {@link JsonSource} thunks. */
75
- isJsonValues?: boolean, previewInput?: ItemPreviewInput<Src> | null, renderInput?: FieldRender | null);
99
+ isJsonValues?: boolean, previewInput?: ItemPreviewInput<Src> | null, renderInput?: FieldRender | null,
100
+ /**
101
+ * Set by `.external(label)`; the entries live behind that adapter.
102
+ *
103
+ * LAST on purpose: every other `new RecordSchema(...,
104
+ this.externalLabel,
105
+ )` in the codebase
106
+ * passes its arguments positionally, so a new parameter anywhere else
107
+ * silently shifts `previewInput` and `renderInput` at each of those call
108
+ * sites.
109
+ */
110
+ externalLabel?: string | null);
76
111
  describe(description: string | null): RecordSchema<T, K, Src>;
77
112
  validate(validationFunction: (src: Src) => false | string): RecordSchema<T, K, Src>;
78
113
  protected executeValidate(path: SourcePath, src: Src): ValidationErrors;
@@ -84,7 +119,17 @@ export declare class RecordSchema<T extends Schema<SelectorSource>, K extends Sc
84
119
  private validateMediaMimeType;
85
120
  protected executeAssert(path: SourcePath, src: unknown): SchemaAssertResult<Src>;
86
121
  nullable(): RecordSchema<T, K, Src | null>;
87
- readonly(isReadonly?: boolean): RecordSchema<T, K, Src>;
122
+ /**
123
+ * A record you can look at but not change.
124
+ *
125
+ * The flag rides in the TYPE as well as in the instance, so `.external()` can
126
+ * freeze it into the source marker and the adapter's write methods become
127
+ * required or forbidden on the strength of it. That is also why `.readonly()`
128
+ * must come BEFORE `.external()`: afterwards the value is a `Schema`, and
129
+ * there is no `.readonly()` left to call — which is the error the compiler
130
+ * gives, and a clearer one than a runtime throw.
131
+ */
132
+ readonly<B extends boolean = true>(isReadonly?: B): RecordSchema<T, K, Src, B>;
88
133
  hidden(isHidden?: boolean): RecordSchema<T, K, Src>;
89
134
  router(router: ValRouter): RecordSchema<T, K, Src>;
90
135
  remote(): RecordSchema<T, K, Src>;
@@ -103,6 +148,37 @@ export declare class RecordSchema<T extends Schema<SelectorSource>, K extends Sc
103
148
  * substitution and content validation are all root-only.
104
149
  */
105
150
  jsonValues(): RecordSchema<T, K, JsonValuesRecordSrc<T, K>>;
151
+ /**
152
+ * Store this record's entries behind an adapter — a database, an HTTP API, a
153
+ * bucket — instead of in the module.
154
+ *
155
+ * Available on every record-derived schema, which is `s.record()`,
156
+ * `s.router()`, `s.images()` and `s.files()` alike: a router is a
157
+ * `RecordSchema` with a `ValRouter`, a gallery one with media options, so all
158
+ * four get external storage from this one method.
159
+ *
160
+ * `label` does not address the binding — the registry is keyed by module, and
161
+ * so is the wire protocol. It exists so a reader of the schema can see what
162
+ * backs it, and it is checked against the binding that claims it, so the two
163
+ * cannot drift.
164
+ *
165
+ * Only supported on a module's ROOT record, exactly as `.jsonValues()` is: a
166
+ * nested external record would leave a hole in the middle of a module's
167
+ * source, and the Studio, patching and validation all assume a module's
168
+ * source is complete apart from its entries.
169
+ */
170
+ external<L extends string>(label: L): RecordSchema<T, K, ExternalRecordSrc<SelectorOfSchema<T>, L, RO>, RO>;
171
+ /**
172
+ * An external record's source is either the `c.external()` marker or entries
173
+ * still written inline in the `.val.ts`.
174
+ *
175
+ * Inline entries are NOT a type error (see `ExternalRecordWritableSrc`), so
176
+ * this is where they surface: as an `external:upload` fix, per key, the way
177
+ * `.jsonValues()` reports an un-extracted entry. Reporting per key rather than
178
+ * once for the record is what lets the fix be applied to one entry, and what
179
+ * puts the error at a path the editor can navigate to.
180
+ */
181
+ private validateExternal;
106
182
  private getRouterValidations;
107
183
  protected executeCustomValidateAt(path: SourcePath, src: Src): ValidationError[];
108
184
  protected executeSerialize(): SerializedRecordSchema;
@@ -1,2 +1,2 @@
1
- export declare const ValidationFix: readonly ["image:add-metadata", "image:check-metadata", "image:upload-remote", "image:download-remote", "image:check-remote", "images:check-remote", "images:upload-remote", "file:add-metadata", "file:check-metadata", "file:upload-remote", "file:download-remote", "file:check-remote", "files:check-remote", "files:upload-remote", "keyof:check-keys", "router:check-route", "images:check-unique-folder", "files:check-unique-folder", "images:check-all-files", "files:check-all-files", "jsonValues:extract-entry"];
1
+ export declare const ValidationFix: readonly ["image:add-metadata", "image:check-metadata", "image:upload-remote", "image:download-remote", "image:check-remote", "images:check-remote", "images:upload-remote", "file:add-metadata", "file:check-metadata", "file:upload-remote", "file:download-remote", "file:check-remote", "files:check-remote", "files:upload-remote", "keyof:check-keys", "router:check-route", "images:check-unique-folder", "files:check-unique-folder", "images:check-all-files", "files:check-all-files", "jsonValues:extract-entry", "external:upload"];
2
2
  export type ValidationFix = (typeof ValidationFix)[number];
@@ -12,11 +12,12 @@ import { MediaSource } from "../source/media.js";
12
12
  import { AllRichTextOptions, RichTextSource } from "../source/richtext.js";
13
13
  import { RichTextSelector } from "./richtext.js";
14
14
  import { JsonSource } from "../source/json.js";
15
+ import { ExternalRecordSrc } from "../source/external.js";
15
16
  import { SettingsSource } from "../source/settings.js";
16
- export type Selector<T extends Source> = Source extends T ? GenericSelector<T> : T extends MediaSource ? GenericSelector<T> : T extends JsonSource ? GenericSelector<JsonSource> : T extends RichTextSource<infer O> ? RichTextSelector<O> : T extends SourceObject ? ObjectSelector<T> : T extends SourceArray ? ArraySelector<T> : T extends SettingsSource ? GenericSelector<T> : T extends string ? StringSelector<T> : T extends number ? NumberSelector<T> : T extends boolean ? BooleanSelector<T> : T extends null ? PrimitiveSelector<null> : never;
17
+ export type Selector<T extends Source> = Source extends T ? GenericSelector<T> : T extends MediaSource ? GenericSelector<T> : T extends JsonSource ? GenericSelector<JsonSource> : T extends ExternalRecordSrc ? GenericSelector<T> : T extends RichTextSource<infer O> ? RichTextSelector<O> : T extends SourceObject ? ObjectSelector<T> : T extends SourceArray ? ArraySelector<T> : T extends SettingsSource ? GenericSelector<T> : T extends string ? StringSelector<T> : T extends number ? NumberSelector<T> : T extends boolean ? BooleanSelector<T> : T extends null ? PrimitiveSelector<null> : never;
17
18
  export type SelectorSource = SourcePrimitive | undefined | readonly SelectorSource[] | {
18
19
  [key: string]: SelectorSource;
19
- } | MediaSource | JsonSource | SettingsSource | RichTextSource<AllRichTextOptions> | GenericSelector<Source>;
20
+ } | MediaSource | JsonSource | ExternalRecordSrc | SettingsSource | RichTextSource<AllRichTextOptions> | GenericSelector<Source>;
20
21
  /**
21
22
  * @internal
22
23
  */
@@ -0,0 +1,91 @@
1
+ import { Source, VAL_EXTENSION } from "./index.js";
2
+ /**
3
+ * The string used as the `_type` discriminator of an {@link ExternalRecordSrc}.
4
+ */
5
+ export declare const EXTERNAL_VAL_EXTENSION_TAG: "external";
6
+ /**
7
+ * Type-level-only slots. Declared here rather than reusing `PhantomType`
8
+ * (`source/index.ts`) because three separate facts have to ride along, and that
9
+ * helper carries one.
10
+ *
11
+ * None of them exist at runtime: `c.external()` produces `{ _type: "external" }`
12
+ * and nothing else.
13
+ */
14
+ declare const ExternalItem: unique symbol;
15
+ declare const ExternalLabel: unique symbol;
16
+ declare const ExternalReadonly: unique symbol;
17
+ /**
18
+ * The source of a record whose entries live behind an adapter — a database, an
19
+ * HTTP API, a bucket — instead of in the module.
20
+ *
21
+ * This is what `c.external()` returns, and it is the counterpart of the
22
+ * `c.json(() => import(...))` marker a `.jsonValues()` entry carries: a value
23
+ * that says "the content is not here" without saying anything about what is.
24
+ * The schema says *which* adapter (`.external("posts")`); this says *that* the
25
+ * entries are elsewhere, which is what every source-walking consumer needs to
26
+ * know without holding a schema.
27
+ *
28
+ * Three phantom parameters, each earning its place:
29
+ *
30
+ * - `T` — the item type. Without it `c.define` would return a module whose type
31
+ * is "a marker", the item type would be gone, and an adapter could not be
32
+ * typed from the schema at all.
33
+ * - `L` — the label from `.external(label)`, so a binding can be checked against
34
+ * the schema that declared it.
35
+ * - `RO` — whether the schema said `.readonly()`, which decides whether the
36
+ * adapter must implement writes or is forbidden from doing so.
37
+ *
38
+ * The defaults are the WIDEST instantiation on purpose: the bare name appears in
39
+ * the `Source` and `SelectorSource` unions, and every narrower instantiation has
40
+ * to be assignable to it. The `external()` factory below defaults the other way,
41
+ * exactly as `JsonSource<T = unknown>` and `json<T = Json>()` do.
42
+ */
43
+ export type ExternalRecordSrc<T = unknown, L extends string = string, RO extends boolean = boolean> = {
44
+ readonly [VAL_EXTENSION]: typeof EXTERNAL_VAL_EXTENSION_TAG;
45
+ } & {
46
+ readonly [ExternalItem]: T;
47
+ readonly [ExternalLabel]: L;
48
+ readonly [ExternalReadonly]: RO;
49
+ };
50
+ /**
51
+ * The marker written as a module's source when its record is `.external()`.
52
+ *
53
+ * Takes no arguments: everything it carries is inferred from the schema it is
54
+ * checked against in `c.define`. The assertion mirrors `json()` — the phantom
55
+ * slots are symbol-keyed and have no runtime representation, so there is nothing
56
+ * to construct for them.
57
+ */
58
+ export declare function external<T = never, L extends string = string, RO extends boolean = false>(): ExternalRecordSrc<T, L, RO>;
59
+ export declare function isExternal(obj: unknown): obj is ExternalRecordSrc;
60
+ /**
61
+ * The item type an external record holds, read back off a module.
62
+ *
63
+ * `GenericSelector` is not imported here — that would be a cycle through
64
+ * `../selector` — so this is written against the phantom slot directly and the
65
+ * selector-shaped overloads live where the selector types do.
66
+ */
67
+ export type ExternalItemOf<S> = S extends ExternalRecordSrc<infer I, string, boolean> ? I : never;
68
+ /** The label an external record was declared with. */
69
+ export type ExternalLabelOf<S> = S extends ExternalRecordSrc<unknown, infer L, boolean> ? L : never;
70
+ /** Whether the schema behind an external record said `.readonly()`. */
71
+ export type ExternalReadonlyOf<S> = S extends ExternalRecordSrc<unknown, string, infer RO> ? RO : never;
72
+ /**
73
+ * The source of an external record as it may be WRITTEN.
74
+ *
75
+ * Either the marker, or entries inline in the `.val.ts`. Inline entries are
76
+ * accepted by the type on purpose, for the same reason `.jsonValues()` accepts
77
+ * them (see `JsonValuesRecordSrc`): pasting content straight into the module is
78
+ * the natural first thing to write, and a type error there is a dead end — the
79
+ * author cannot see what to write instead. Validation reports the inline entries
80
+ * as an `external:upload` fix instead, so the mistake is repaired rather than
81
+ * blocking authoring, and they are readable at runtime meanwhile.
82
+ */
83
+ export type ExternalRecordWritableSrc<Item, Key extends string = string, L extends string = string, RO extends boolean = boolean> = ExternalRecordSrc<Item, L, RO> | Record<Key, Item>;
84
+ /**
85
+ * Is this module source an external record that still has entries written
86
+ * inline? Used by validation to raise `external:upload`.
87
+ */
88
+ export declare function hasInlineExternalEntries(source: Source): source is {
89
+ [key: string]: Source;
90
+ };
91
+ export {};
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The reference stored for a file whose bytes live behind an external adapter.
3
+ *
4
+ * Deliberately the same shape of idea as a remote ref (`remote/splitRemoteRef`):
5
+ * a URL-ish string that **ends with the file's virtual path**, so the file's
6
+ * logical identity survives wherever its bytes happen to be.
7
+ *
8
+ * /api/val/external/documents/f/{fileHash}/p/public/val/documents/report_a1b2c.pdf
9
+ * \__________________________/ \________/ \_______________________________/
10
+ * route + label content the path a LOCAL file
11
+ * address would have had
12
+ *
13
+ * Three things follow from that shape, and each is load-bearing:
14
+ *
15
+ * - **`media.ts` needs no change.** `isRemote` is `!path.startsWith("/public")`,
16
+ * and this does not, so `Internal.mediaUrl` returns the ref as-is and Val's
17
+ * route serves it. The media rule that has been got wrong repeatedly is left
18
+ * exactly alone.
19
+ * - **Moving between storage modes is mechanical.** External → local reads the
20
+ * `public/…` suffix back out, fetches the bytes and writes them there; local →
21
+ * external uploads the file already at that path. Nothing is renamed.
22
+ * - **The filename is Val's, not the adapter's**, produced by the same
23
+ * `createFilename` a local or remote file uses — so the same bytes get the
24
+ * same name under all three storage modes.
25
+ */
26
+ export type ExternalFileRefParts = {
27
+ /** The `.external(label)` of the record the file belongs to. */
28
+ label: string;
29
+ /** SHA-256 of the bytes. Content-addressed, so a retried publish re-uses. */
30
+ fileHash: string;
31
+ /** The virtual path, WITHOUT a leading slash: `public/val/...`. */
32
+ filePath: `public/${string}`;
33
+ };
34
+ export declare function createExternalFileRef({ label, fileHash, filePath, }: {
35
+ label: string;
36
+ fileHash: string;
37
+ /** Accepts the path as it is written in a module: `/public/val/...`. */
38
+ filePath: string;
39
+ }): string;
40
+ export declare function splitExternalFileRef(ref: string): ({
41
+ status: "success";
42
+ } & ExternalFileRefParts) | {
43
+ status: "error";
44
+ error: string;
45
+ };
46
+ export declare function isExternalFileRef(ref: string): boolean;
@@ -1,8 +1,9 @@
1
+ import { ExternalRecordSrc } from "./external.js";
1
2
  import { JsonSource } from "./json.js";
2
3
  import { MediaSource } from "./media.js";
3
4
  import { RichTextOptions, RichTextSource } from "./richtext.js";
4
5
  import { SettingsSource } from "./settings.js";
5
- export type Source = SourcePrimitive | SourceObject | SourceArray | MediaSource | JsonSource | SettingsSource | RichTextSource<RichTextOptions>;
6
+ export type Source = SourcePrimitive | SourceObject | SourceArray | MediaSource | JsonSource | ExternalRecordSrc | SettingsSource | RichTextSource<RichTextOptions>;
6
7
  export type SourceObject = {
7
8
  [key in string]: Source;
8
9
  } & {
@@ -22,7 +23,7 @@ export type SourceObject = {
22
23
  export type SourceArray = readonly Source[];
23
24
  export type SourcePrimitive = string | number | boolean | null;
24
25
  export declare const VAL_EXTENSION: "_type";
25
- export declare function getValExtension(source: Source): false | "" | 0 | "json" | null | undefined;
26
+ export declare function getValExtension(source: Source): false | "" | 0 | "json" | "external" | null | undefined;
26
27
  /**
27
28
  * A phantom type parameter is one that doesn't show up at runtime, but is checked statically (and only) at compile time.
28
29
  *