@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.
@@ -1964,7 +1964,7 @@ var union = function union(key) {
1964
1964
  return new UnionSchema(key, objects);
1965
1965
  };
1966
1966
 
1967
- var RegEx = /^(https?:\/\/[^/]+)\/file\/p\/([^/]+)\/b\/([^/]+)\/v\/([^/]+)\/h\/([^/]+)\/f\/([^/]+)\/p\/(.+)$/;
1967
+ var RegEx$1 = /^(https?:\/\/[^/]+)\/file\/p\/([^/]+)\/b\/([^/]+)\/v\/([^/]+)\/h\/([^/]+)\/f\/([^/]+)\/p\/(.+)$/;
1968
1968
  function splitRemoteRef(ref) {
1969
1969
  if (ref[0] === "/") {
1970
1970
  return {
@@ -1972,7 +1972,7 @@ function splitRemoteRef(ref) {
1972
1972
  error: "Not a remote ref: " + ref
1973
1973
  };
1974
1974
  }
1975
- var match = ref.match(RegEx);
1975
+ var match = ref.match(RegEx$1);
1976
1976
  if (!match) {
1977
1977
  return {
1978
1978
  status: "error",
@@ -4370,6 +4370,94 @@ function _resolveJsonValues() {
4370
4370
  return _resolveJsonValues.apply(this, arguments);
4371
4371
  }
4372
4372
 
4373
+ /**
4374
+ * The string used as the `_type` discriminator of an {@link ExternalRecordSrc}.
4375
+ */
4376
+ var EXTERNAL_VAL_EXTENSION_TAG = "external";
4377
+
4378
+ /**
4379
+ * Type-level-only slots. Declared here rather than reusing `PhantomType`
4380
+ * (`source/index.ts`) because three separate facts have to ride along, and that
4381
+ * helper carries one.
4382
+ *
4383
+ * None of them exist at runtime: `c.external()` produces `{ _type: "external" }`
4384
+ * and nothing else.
4385
+ */
4386
+
4387
+ /**
4388
+ * The source of a record whose entries live behind an adapter — a database, an
4389
+ * HTTP API, a bucket — instead of in the module.
4390
+ *
4391
+ * This is what `c.external()` returns, and it is the counterpart of the
4392
+ * `c.json(() => import(...))` marker a `.jsonValues()` entry carries: a value
4393
+ * that says "the content is not here" without saying anything about what is.
4394
+ * The schema says *which* adapter (`.external("posts")`); this says *that* the
4395
+ * entries are elsewhere, which is what every source-walking consumer needs to
4396
+ * know without holding a schema.
4397
+ *
4398
+ * Three phantom parameters, each earning its place:
4399
+ *
4400
+ * - `T` — the item type. Without it `c.define` would return a module whose type
4401
+ * is "a marker", the item type would be gone, and an adapter could not be
4402
+ * typed from the schema at all.
4403
+ * - `L` — the label from `.external(label)`, so a binding can be checked against
4404
+ * the schema that declared it.
4405
+ * - `RO` — whether the schema said `.readonly()`, which decides whether the
4406
+ * adapter must implement writes or is forbidden from doing so.
4407
+ *
4408
+ * The defaults are the WIDEST instantiation on purpose: the bare name appears in
4409
+ * the `Source` and `SelectorSource` unions, and every narrower instantiation has
4410
+ * to be assignable to it. The `external()` factory below defaults the other way,
4411
+ * exactly as `JsonSource<T = unknown>` and `json<T = Json>()` do.
4412
+ */
4413
+
4414
+ /**
4415
+ * The marker written as a module's source when its record is `.external()`.
4416
+ *
4417
+ * Takes no arguments: everything it carries is inferred from the schema it is
4418
+ * checked against in `c.define`. The assertion mirrors `json()` — the phantom
4419
+ * slots are symbol-keyed and have no runtime representation, so there is nothing
4420
+ * to construct for them.
4421
+ */
4422
+ function external() {
4423
+ return _defineProperty({}, VAL_EXTENSION, EXTERNAL_VAL_EXTENSION_TAG);
4424
+ }
4425
+ function isExternal(obj) {
4426
+ return _typeof(obj) === "object" && obj !== null && VAL_EXTENSION in obj && obj[VAL_EXTENSION] === EXTERNAL_VAL_EXTENSION_TAG;
4427
+ }
4428
+
4429
+ /**
4430
+ * The item type an external record holds, read back off a module.
4431
+ *
4432
+ * `GenericSelector` is not imported here — that would be a cycle through
4433
+ * `../selector` — so this is written against the phantom slot directly and the
4434
+ * selector-shaped overloads live where the selector types do.
4435
+ */
4436
+
4437
+ /** The label an external record was declared with. */
4438
+
4439
+ /** Whether the schema behind an external record said `.readonly()`. */
4440
+
4441
+ /**
4442
+ * The source of an external record as it may be WRITTEN.
4443
+ *
4444
+ * Either the marker, or entries inline in the `.val.ts`. Inline entries are
4445
+ * accepted by the type on purpose, for the same reason `.jsonValues()` accepts
4446
+ * them (see `JsonValuesRecordSrc`): pasting content straight into the module is
4447
+ * the natural first thing to write, and a type error there is a dead end — the
4448
+ * author cannot see what to write instead. Validation reports the inline entries
4449
+ * as an `external:upload` fix instead, so the mistake is repaired rather than
4450
+ * blocking authoring, and they are readable at runtime meanwhile.
4451
+ */
4452
+
4453
+ /**
4454
+ * Is this module source an external record that still has entries written
4455
+ * inline? Used by validation to raise `external:upload`.
4456
+ */
4457
+ function hasInlineExternalEntries(source) {
4458
+ return _typeof(source) === "object" && source !== null && !Array.isArray(source) && !isExternal(source) && Object.keys(source).length > 0;
4459
+ }
4460
+
4373
4461
  /**
4374
4462
  * The source type of a `.jsonValues()` record: every entry value is EITHER a
4375
4463
  * lazily loaded {@link JsonSource} whose resolved content is the (loosened, see
@@ -4398,6 +4486,7 @@ var RecordSchema = /*#__PURE__*/function (_Schema) {
4398
4486
  var isJsonValues = arguments.length > 9 && arguments[9] !== undefined ? arguments[9] : false;
4399
4487
  var previewInput = arguments.length > 10 && arguments[10] !== undefined ? arguments[10] : null;
4400
4488
  var renderInput = arguments.length > 11 && arguments[11] !== undefined ? arguments[11] : null;
4489
+ var externalLabel = arguments.length > 12 && arguments[12] !== undefined ? arguments[12] : null;
4401
4490
  _classCallCheck(this, RecordSchema);
4402
4491
  _this = _callSuper(this, RecordSchema);
4403
4492
  _this.item = item;
@@ -4412,24 +4501,28 @@ var RecordSchema = /*#__PURE__*/function (_Schema) {
4412
4501
  _this.isJsonValues = isJsonValues;
4413
4502
  _this.previewInput = previewInput;
4414
4503
  _this.renderInput = renderInput;
4504
+ _this.externalLabel = externalLabel;
4415
4505
  return _this;
4416
4506
  }
4417
4507
  _inherits(RecordSchema, _Schema);
4418
4508
  return _createClass(RecordSchema, [{
4419
4509
  key: "describe",
4420
4510
  value: function describe(description) {
4421
- return new RecordSchema(this.item, this.opt, this.customValidateFunctions, this.currentRouter, this.keySchema, this.mediaOptions, this.isReadonly, this.isHidden, description !== null && description !== void 0 ? description : undefined, this.isJsonValues, this.previewInput, this.renderInput);
4511
+ return new RecordSchema(this.item, this.opt, this.customValidateFunctions, this.currentRouter, this.keySchema, this.mediaOptions, this.isReadonly, this.isHidden, description !== null && description !== void 0 ? description : undefined, this.isJsonValues, this.previewInput, this.renderInput, this.externalLabel);
4422
4512
  }
4423
4513
  }, {
4424
4514
  key: "validate",
4425
4515
  value: function validate(validationFunction) {
4426
- return new RecordSchema(this.item, this.opt, [].concat(_toConsumableArray(this.customValidateFunctions), [validationFunction]), this.currentRouter, this.keySchema, this.mediaOptions, this.isReadonly, this.isHidden, this.description, this.isJsonValues, this.previewInput, this.renderInput);
4516
+ return new RecordSchema(this.item, this.opt, [].concat(_toConsumableArray(this.customValidateFunctions), [validationFunction]), this.currentRouter, this.keySchema, this.mediaOptions, this.isReadonly, this.isHidden, this.description, this.isJsonValues, this.previewInput, this.renderInput, this.externalLabel);
4427
4517
  }
4428
4518
  }, {
4429
4519
  key: "executeValidate",
4430
4520
  value: function executeValidate(path, src) {
4431
4521
  var _this2 = this;
4432
4522
  var error = false;
4523
+ if (this.externalLabel !== null) {
4524
+ return this.validateExternal(path, src);
4525
+ }
4433
4526
  var customValidationErrors = this.executeCustomValidateFunctions(src, this.customValidateFunctions, {
4434
4527
  path: path
4435
4528
  });
@@ -4766,31 +4859,42 @@ var RecordSchema = /*#__PURE__*/function (_Schema) {
4766
4859
  }, {
4767
4860
  key: "nullable",
4768
4861
  value: function nullable() {
4769
- return new RecordSchema(this.item, true, this.customValidateFunctions, this.currentRouter, this.keySchema, this.mediaOptions, this.isReadonly, this.isHidden, this.description, this.isJsonValues, this.previewInput, this.renderInput);
4862
+ return new RecordSchema(this.item, true, this.customValidateFunctions, this.currentRouter, this.keySchema, this.mediaOptions, this.isReadonly, this.isHidden, this.description, this.isJsonValues, this.previewInput, this.renderInput, this.externalLabel);
4770
4863
  }
4864
+
4865
+ /**
4866
+ * A record you can look at but not change.
4867
+ *
4868
+ * The flag rides in the TYPE as well as in the instance, so `.external()` can
4869
+ * freeze it into the source marker and the adapter's write methods become
4870
+ * required or forbidden on the strength of it. That is also why `.readonly()`
4871
+ * must come BEFORE `.external()`: afterwards the value is a `Schema`, and
4872
+ * there is no `.readonly()` left to call — which is the error the compiler
4873
+ * gives, and a clearer one than a runtime throw.
4874
+ */
4771
4875
  }, {
4772
4876
  key: "readonly",
4773
4877
  value: function readonly() {
4774
4878
  var isReadonly = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
4775
- return new RecordSchema(this.item, this.opt, this.customValidateFunctions, this.currentRouter, this.keySchema, this.mediaOptions, isReadonly, this.isHidden, this.description, this.isJsonValues, this.previewInput, this.renderInput);
4879
+ return new RecordSchema(this.item, this.opt, this.customValidateFunctions, this.currentRouter, this.keySchema, this.mediaOptions, isReadonly, this.isHidden, this.description, this.isJsonValues, this.previewInput, this.renderInput, this.externalLabel);
4776
4880
  }
4777
4881
  }, {
4778
4882
  key: "hidden",
4779
4883
  value: function hidden() {
4780
4884
  var isHidden = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
4781
- return new RecordSchema(this.item, this.opt, this.customValidateFunctions, this.currentRouter, this.keySchema, this.mediaOptions, this.isReadonly, isHidden, this.description, this.isJsonValues, this.previewInput, this.renderInput);
4885
+ return new RecordSchema(this.item, this.opt, this.customValidateFunctions, this.currentRouter, this.keySchema, this.mediaOptions, this.isReadonly, isHidden, this.description, this.isJsonValues, this.previewInput, this.renderInput, this.externalLabel);
4782
4886
  }
4783
4887
  }, {
4784
4888
  key: "router",
4785
4889
  value: function router(_router) {
4786
- return new RecordSchema(this.item, this.opt, this.customValidateFunctions, _router, this.keySchema, this.mediaOptions, this.isReadonly, this.isHidden, this.description, this.isJsonValues, this.previewInput, this.renderInput);
4890
+ return new RecordSchema(this.item, this.opt, this.customValidateFunctions, _router, this.keySchema, this.mediaOptions, this.isReadonly, this.isHidden, this.description, this.isJsonValues, this.previewInput, this.renderInput, this.externalLabel);
4787
4891
  }
4788
4892
  }, {
4789
4893
  key: "remote",
4790
4894
  value: function remote() {
4791
4895
  return new RecordSchema(this.item, this.opt, this.customValidateFunctions, this.currentRouter, this.keySchema, this.mediaOptions ? _objectSpread2(_objectSpread2({}, this.mediaOptions), {}, {
4792
4896
  remote: true
4793
- }) : undefined, this.isReadonly, this.isHidden, this.description, this.isJsonValues, this.previewInput, this.renderInput);
4897
+ }) : undefined, this.isReadonly, this.isHidden, this.description, this.isJsonValues, this.previewInput, this.renderInput, this.externalLabel);
4794
4898
  }
4795
4899
 
4796
4900
  /**
@@ -4832,7 +4936,84 @@ var RecordSchema = /*#__PURE__*/function (_Schema) {
4832
4936
  // Empty by construction: the guard above rejects any that were registered.
4833
4937
  [], this.currentRouter, this.keySchema, this.mediaOptions, this.isReadonly, this.isHidden, this.description, true,
4834
4938
  // Null by construction: the guard above rejects any that was declared.
4835
- null, this.renderInput);
4939
+ null, this.renderInput, this.externalLabel);
4940
+ }
4941
+
4942
+ /**
4943
+ * Store this record's entries behind an adapter — a database, an HTTP API, a
4944
+ * bucket — instead of in the module.
4945
+ *
4946
+ * Available on every record-derived schema, which is `s.record()`,
4947
+ * `s.router()`, `s.images()` and `s.files()` alike: a router is a
4948
+ * `RecordSchema` with a `ValRouter`, a gallery one with media options, so all
4949
+ * four get external storage from this one method.
4950
+ *
4951
+ * `label` does not address the binding — the registry is keyed by module, and
4952
+ * so is the wire protocol. It exists so a reader of the schema can see what
4953
+ * backs it, and it is checked against the binding that claims it, so the two
4954
+ * cannot drift.
4955
+ *
4956
+ * Only supported on a module's ROOT record, exactly as `.jsonValues()` is: a
4957
+ * nested external record would leave a hole in the middle of a module's
4958
+ * source, and the Studio, patching and validation all assume a module's
4959
+ * source is complete apart from its entries.
4960
+ */
4961
+ }, {
4962
+ key: "external",
4963
+ value: function external(label) {
4964
+ if (this.isJsonValues) {
4965
+ throw new Error(".external() cannot be combined with .jsonValues(): entries live in one place or the other, not both.");
4966
+ }
4967
+ if (!label) {
4968
+ throw new Error('.external() requires a label, e.g. .external("posts")');
4969
+ }
4970
+ return new RecordSchema(this.item, this.opt,
4971
+ // Empty by construction: a validator declared before `.external()` is
4972
+ // typed against the un-external source shape and cannot be carried over.
4973
+ [], this.currentRouter, this.keySchema, this.mediaOptions, this.isReadonly, this.isHidden, this.description, this.isJsonValues, null, this.renderInput, label);
4974
+ }
4975
+
4976
+ /**
4977
+ * An external record's source is either the `c.external()` marker or entries
4978
+ * still written inline in the `.val.ts`.
4979
+ *
4980
+ * Inline entries are NOT a type error (see `ExternalRecordWritableSrc`), so
4981
+ * this is where they surface: as an `external:upload` fix, per key, the way
4982
+ * `.jsonValues()` reports an un-extracted entry. Reporting per key rather than
4983
+ * once for the record is what lets the fix be applied to one entry, and what
4984
+ * puts the error at a path the editor can navigate to.
4985
+ */
4986
+ }, {
4987
+ key: "validateExternal",
4988
+ value: function validateExternal(path, src) {
4989
+ if (this.opt && (src === null || src === undefined)) {
4990
+ return false;
4991
+ }
4992
+ if (src === null || _typeof(src) !== "object" || Array.isArray(src)) {
4993
+ return _defineProperty({}, path, [{
4994
+ message: "Expected the c.external() marker or inline entries, got '".concat(src === null ? "null" : Array.isArray(src) ? "array" : _typeof(src), "'"),
4995
+ value: src,
4996
+ schemaError: true
4997
+ }]);
4998
+ }
4999
+ if (isExternal(src)) {
5000
+ // The marker: the entries are the adapter's, and there is nothing here to
5001
+ // check. Their content is validated as it is read.
5002
+ return false;
5003
+ }
5004
+ var error = false;
5005
+ for (var _i = 0, _Object$keys = Object.keys(src); _i < _Object$keys.length; _i++) {
5006
+ var _key = _Object$keys[_i];
5007
+ var subPath = createValPathOfItem(path, _key);
5008
+ if (!subPath) {
5009
+ continue;
5010
+ }
5011
+ error = this.appendValidationError(error, subPath, "Entry is written inline, but this record is .external(\"".concat(this.externalLabel, "\"). Run 'val external upload' to move it into the store."), src[_key]);
5012
+ var errors = error;
5013
+ var last = errors[subPath][errors[subPath].length - 1];
5014
+ last.fixes = ["external:upload"];
5015
+ }
5016
+ return error;
4836
5017
  }
4837
5018
  }, {
4838
5019
  key: "getRouterValidations",
@@ -4886,7 +5067,7 @@ var RecordSchema = /*#__PURE__*/function (_Schema) {
4886
5067
  }, {
4887
5068
  key: "executeSerialize",
4888
5069
  value: function executeSerialize() {
4889
- var _this$renderInput, _this$keySchema, _this$currentRouter, _this$customValidateF;
5070
+ var _this$renderInput, _this$keySchema, _this$currentRouter, _this$customValidateF, _this$externalLabel;
4890
5071
  var result = {
4891
5072
  type: "record",
4892
5073
  render: (_this$renderInput = this.renderInput) !== null && _this$renderInput !== void 0 ? _this$renderInput : undefined,
@@ -4897,6 +5078,7 @@ var RecordSchema = /*#__PURE__*/function (_Schema) {
4897
5078
  router: (_this$currentRouter = this.currentRouter) === null || _this$currentRouter === void 0 ? void 0 : _this$currentRouter.getRouterId(),
4898
5079
  customValidate: this.customValidateFunctions && ((_this$customValidateF = this.customValidateFunctions) === null || _this$customValidateF === void 0 ? void 0 : _this$customValidateF.length) > 0,
4899
5080
  jsonValues: this.isJsonValues ? true : undefined,
5081
+ external: (_this$externalLabel = this.externalLabel) !== null && _this$externalLabel !== void 0 ? _this$externalLabel : undefined,
4900
5082
  readonly: this.isReadonly,
4901
5083
  hidden: this.isHidden,
4902
5084
  description: this.description
@@ -4934,8 +5116,12 @@ var RecordSchema = /*#__PURE__*/function (_Schema) {
4934
5116
  if (src === null) {
4935
5117
  return res;
4936
5118
  }
4937
- for (var _key in src) {
4938
- var itemSrc = src[_key];
5119
+ if (isExternal(src)) {
5120
+ // The entries are the adapter's; there is nothing here to preview.
5121
+ return res;
5122
+ }
5123
+ for (var _key2 in src) {
5124
+ var itemSrc = src[_key2];
4939
5125
  if (itemSrc === null || itemSrc === undefined) {
4940
5126
  continue;
4941
5127
  }
@@ -4946,14 +5132,14 @@ var RecordSchema = /*#__PURE__*/function (_Schema) {
4946
5132
  // keys, and the caller shows a placeholder for the rest.
4947
5133
  continue;
4948
5134
  }
4949
- var subPath = unsafeCreateSourcePath(sourcePath, _key);
5135
+ var subPath = unsafeCreateSourcePath(sourcePath, _key2);
4950
5136
  if (scope !== undefined && !scope.wantsUnder(subPath)) {
4951
5137
  continue;
4952
5138
  }
4953
5139
  var itemResult = this.item["executePreview"](subPath, itemSrc, scope);
4954
5140
  for (var keyS in itemResult) {
4955
- var _key2 = keyS;
4956
- res[_key2] = itemResult[_key2];
5141
+ var _key3 = keyS;
5142
+ res[_key3] = itemResult[_key3];
4957
5143
  }
4958
5144
  }
4959
5145
  // The entries preview comes from the ITEM schema's own `preview` — the
@@ -4964,9 +5150,9 @@ var RecordSchema = /*#__PURE__*/function (_Schema) {
4964
5150
  // is being shown, only the wanted entries when it is not.
4965
5151
  var window = scope !== undefined && !scope.wants(sourcePath) ? scope : null;
4966
5152
  var items = [];
4967
- for (var _i = 0, _Object$entries = Object.entries(src); _i < _Object$entries.length; _i++) {
4968
- var _Object$entries$_i = _slicedToArray(_Object$entries[_i], 2),
4969
- _key3 = _Object$entries$_i[0],
5153
+ for (var _i2 = 0, _Object$entries = Object.entries(src); _i2 < _Object$entries.length; _i2++) {
5154
+ var _Object$entries$_i = _slicedToArray(_Object$entries[_i2], 2),
5155
+ _key4 = _Object$entries$_i[0],
4970
5156
  val = _Object$entries$_i[1];
4971
5157
  if (isJson(val)) {
4972
5158
  continue; // as above: nothing to select from an un-loaded entry
@@ -4974,7 +5160,7 @@ var RecordSchema = /*#__PURE__*/function (_Schema) {
4974
5160
  if (val === null || val === undefined) {
4975
5161
  continue;
4976
5162
  }
4977
- if (window !== null && !window.wantsUnder(unsafeCreateSourcePath(sourcePath, _key3))) {
5163
+ if (window !== null && !window.wantsUnder(unsafeCreateSourcePath(sourcePath, _key4))) {
4978
5164
  continue;
4979
5165
  }
4980
5166
  // Per KEY, not per record: the closure is user code, and one entry whose
@@ -4986,14 +5172,14 @@ var RecordSchema = /*#__PURE__*/function (_Schema) {
4986
5172
  var title = _item.title,
4987
5173
  subtitle = _item.subtitle,
4988
5174
  image = _item.image;
4989
- items.push([_key3, {
5175
+ items.push([_key4, {
4990
5176
  title: title,
4991
5177
  subtitle: subtitle,
4992
5178
  image: image
4993
5179
  }]);
4994
5180
  }
4995
5181
  } catch (e) {
4996
- res[unsafeCreateSourcePath(sourcePath, _key3)] = {
5182
+ res[unsafeCreateSourcePath(sourcePath, _key4)] = {
4997
5183
  status: "error",
4998
5184
  message: e instanceof Error ? e.message : "Unknown error"
4999
5185
  };
@@ -5034,7 +5220,7 @@ var RecordSchema = /*#__PURE__*/function (_Schema) {
5034
5220
  }, {
5035
5221
  key: "preview",
5036
5222
  value: function preview(select) {
5037
- return new RecordSchema(this.item, this.opt, this.customValidateFunctions, this.currentRouter, this.keySchema, this.mediaOptions, this.isReadonly, this.isHidden, this.description, this.isJsonValues, select, this.renderInput);
5223
+ return new RecordSchema(this.item, this.opt, this.customValidateFunctions, this.currentRouter, this.keySchema, this.mediaOptions, this.isReadonly, this.isHidden, this.description, this.isJsonValues, select, this.renderInput, this.externalLabel);
5038
5224
  }
5039
5225
 
5040
5226
  /**
@@ -5047,7 +5233,7 @@ var RecordSchema = /*#__PURE__*/function (_Schema) {
5047
5233
  }, {
5048
5234
  key: "render",
5049
5235
  value: function render(input) {
5050
- return new RecordSchema(this.item, this.opt, this.customValidateFunctions, this.currentRouter, this.keySchema, this.mediaOptions, this.isReadonly, this.isHidden, this.description, this.isJsonValues, this.previewInput, input);
5236
+ return new RecordSchema(this.item, this.opt, this.customValidateFunctions, this.currentRouter, this.keySchema, this.mediaOptions, this.isReadonly, this.isHidden, this.description, this.isJsonValues, this.previewInput, input, this.externalLabel);
5051
5237
  }
5052
5238
  }]);
5053
5239
  }(Schema);
@@ -5548,6 +5734,22 @@ function settings() {
5548
5734
  });
5549
5735
  }
5550
5736
 
5737
+ /**
5738
+ * What may be WRITTEN as an external record's source, on top of the marker.
5739
+ *
5740
+ * Entries inline in the `.val.ts` are accepted on purpose — the same call
5741
+ * `.jsonValues()` makes for the same reason: pasting content straight into the
5742
+ * module is the natural first thing to write, and a type error there is a dead
5743
+ * end, because the author cannot see what to write instead. Validation reports
5744
+ * them as an `external:upload` fix, and they are readable at runtime meanwhile.
5745
+ *
5746
+ * Widened HERE rather than in the schema's own source type so that the MODULE
5747
+ * stays typed on the marker alone. An adapter is bound to a module, and a module
5748
+ * whose type were "marker or plain record" would be indistinguishable from an
5749
+ * ordinary record — which would let `entry()` accept a module that is not
5750
+ * external at all.
5751
+ */
5752
+
5551
5753
  function define(id,
5552
5754
  // TODO: `/${string}`
5553
5755
  schema, source) {
@@ -6955,6 +7157,8 @@ function file(options) {
6955
7157
  /**
6956
7158
  * Define a collection of files.
6957
7159
  *
7160
+ * Remote is off by default: call `.remote()` on the result to allow remote files.
7161
+ *
6958
7162
  * @example
6959
7163
  * ```typescript
6960
7164
  * const schema = s.files({
@@ -6969,7 +7173,6 @@ function file(options) {
6969
7173
  * ```
6970
7174
  */
6971
7175
  var files = function files(options) {
6972
- var _options$remote;
6973
7176
  var directory = options.directory;
6974
7177
  var itemSchema = new ObjectSchema({
6975
7178
  mimeType: new StringSchema({}, false)
@@ -6978,7 +7181,7 @@ var files = function files(options) {
6978
7181
  type: "files",
6979
7182
  accept: options.accept,
6980
7183
  directory: directory,
6981
- remote: (_options$remote = options.remote) !== null && _options$remote !== void 0 ? _options$remote : false
7184
+ remote: false
6982
7185
  });
6983
7186
  };
6984
7187
 
@@ -8346,7 +8549,7 @@ function router(router, keyOrItem, maybeItem) {
8346
8549
  *
8347
8550
  * `directory` is required — it decides where uploads land, so it is not something
8348
8551
  * to be inferred. The rest defaults: any image type (`"image/*"`), nullable alt
8349
- * text, remote disabled.
8552
+ * text, remote disabled. Call `.remote()` on the result to allow remote images.
8350
8553
  *
8351
8554
  * @example
8352
8555
  * ```typescript
@@ -8366,7 +8569,7 @@ function router(router, keyOrItem, maybeItem) {
8366
8569
  * ```
8367
8570
  */
8368
8571
  var images = function images(options) {
8369
- var _options$alt, _options$accept, _options$remote;
8572
+ var _options$alt, _options$accept;
8370
8573
  var directory = options.directory;
8371
8574
  var altSchema = (_options$alt = options.alt) !== null && _options$alt !== void 0 ? _options$alt : string().nullable();
8372
8575
  var itemSchema = new ObjectSchema({
@@ -8379,7 +8582,7 @@ var images = function images(options) {
8379
8582
  type: "images",
8380
8583
  accept: (_options$accept = options.accept) !== null && _options$accept !== void 0 ? _options$accept : "image/*",
8381
8584
  directory: directory,
8382
- remote: (_options$remote = options.remote) !== null && _options$remote !== void 0 ? _options$remote : false,
8585
+ remote: false,
8383
8586
  altSchema: altSchema,
8384
8587
  encode: options.encode
8385
8588
  });
@@ -8457,7 +8660,8 @@ var initVal = function initVal(config) {
8457
8660
  },
8458
8661
  c: {
8459
8662
  define: define,
8460
- json: json
8663
+ json: json,
8664
+ external: external
8461
8665
  },
8462
8666
  s: s,
8463
8667
  config: config
@@ -9254,9 +9458,10 @@ function _extractValModules() {
9254
9458
  * The one answer to that question. There used to be two — `hasRemoteFileSchema`
9255
9459
  * in the server, gating whether `/save` demands remote credentials, and
9256
9460
  * `findRequiredRemoteFiles` in the Studio, gating the `/remote/settings` fetch —
9257
- * and they disagreed about `s.images({ remote: true })`: the Studio's counted a
9258
- * remote media record, the server's did not, because a media collection
9259
- * serializes as a `record` of metadata rather than as an image schema.
9461
+ * and they disagreed about `s.images({ ... }).remote()`: the Studio's version
9462
+ * counted a remote media record, the server's did not, because a media
9463
+ * collection serializes as a `record` of metadata rather than as an image
9464
+ * schema.
9260
9465
  *
9261
9466
  * The server's answer was the wrong one, and silently so. With it false,
9262
9467
  * `/save` runs `saveOrUploadFiles` in `skip-remote` mode, and that mode does not
@@ -9349,6 +9554,85 @@ function hasRemoteFileSchema(schema) {
9349
9554
  }
9350
9555
  }
9351
9556
 
9557
+ /**
9558
+ * Does this schema hold any media at all — remote or not?
9559
+ *
9560
+ * The sibling of {@link hasRemoteFileSchema}, and the same walk minus the
9561
+ * `remote` predicate. It answers the question an external record has to ask
9562
+ * before it will accept a binding: an item schema containing media needs a
9563
+ * `putFile`/`getFile` pair, and without one that is a schema error.
9564
+ *
9565
+ * It is a walk over the SERIALIZED schema for the reasons given above, and for
9566
+ * one more that is specific to this question: the two cases most likely to be
9567
+ * missed are invisible to a value type. A media collection (`s.images()` /
9568
+ * `s.files()`) serializes as a record of metadata with no image schema inside
9569
+ * it, and a richtext inline image lives in a constructor argument. A check
9570
+ * written against the item TYPE would silently pass both — and the gallery case
9571
+ * already has a data-loss incident attached to it (see above).
9572
+ */
9573
+ function hasMediaSchema(schema) {
9574
+ if (schema.type === "file" || schema.type === "image") {
9575
+ return true;
9576
+ } else if (schema.type === "richtext") {
9577
+ var _schema$options2;
9578
+ // `img` is `boolean | SerializedImageSchema`: `true` means inline images are
9579
+ // allowed with the DEFAULT image schema, and is just as much media as a
9580
+ // spelled-out one. `hasRemoteFileSchema` tests only for an object because a
9581
+ // default image is not remote — a different question with a different
9582
+ // answer, which is why these two walks are separate functions.
9583
+ if ((_schema$options2 = schema.options) !== null && _schema$options2 !== void 0 && _schema$options2.img) {
9584
+ return true;
9585
+ }
9586
+ return false;
9587
+ } else if (schema.type === "array") {
9588
+ return hasMediaSchema(schema.item);
9589
+ } else if (schema.type === "record") {
9590
+ // A media collection names its file by the record's KEY, so there is no
9591
+ // image schema inside to find: `mediaType` is where the answer is written.
9592
+ if (schema.mediaType) {
9593
+ return true;
9594
+ }
9595
+ return hasMediaSchema(schema.item);
9596
+ } else if (schema.type === "object") {
9597
+ for (var key in schema.items) {
9598
+ if (hasMediaSchema(schema.items[key])) {
9599
+ return true;
9600
+ }
9601
+ }
9602
+ return false;
9603
+ } else if (schema.type === "union") {
9604
+ var unionObjectSchema = typeof schema.key === "string" ? schema : undefined;
9605
+ if (unionObjectSchema) {
9606
+ var _iterator2 = result._createForOfIteratorHelper(unionObjectSchema.items),
9607
+ _step2;
9608
+ try {
9609
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
9610
+ var item = _step2.value;
9611
+ if (hasMediaSchema(item)) {
9612
+ return true;
9613
+ }
9614
+ }
9615
+ } catch (err) {
9616
+ _iterator2.e(err);
9617
+ } finally {
9618
+ _iterator2.f();
9619
+ }
9620
+ }
9621
+ // A string union's items are literals, so there is nothing to look inside.
9622
+ return false;
9623
+ } else if (schema.type === "boolean" || schema.type === "number" || schema.type === "string" || schema.type === "literal" || schema.type === "date" || schema.type === "dateTime" || schema.type === "color" || schema.type === "code" || schema.type === "keyOf" || schema.type === "route" ||
9624
+ // A settings module holds the assistant's configuration, not content, and
9625
+ // nothing in it is media.
9626
+ schema.type === "settings") {
9627
+ return false;
9628
+ } else {
9629
+ // Same contract as above: adding a schema type without teaching this
9630
+ // function about it must not compile.
9631
+ var exhaustiveCheck = schema;
9632
+ throw new Error("Unexpected schema: ".concat(JSON.stringify(exhaustiveCheck)));
9633
+ }
9634
+ }
9635
+
9352
9636
  var DEFAULT_VAL_REMOTE_HOST = "https://remote.val.build";
9353
9637
 
9354
9638
  var PatchError = /*#__PURE__*/_createClass(function PatchError(message) {
@@ -9493,6 +9777,79 @@ function mediaPathOf(value) {
9493
9777
  return typeof path === "string" ? path : null;
9494
9778
  }
9495
9779
 
9780
+ /**
9781
+ * The reference stored for a file whose bytes live behind an external adapter.
9782
+ *
9783
+ * Deliberately the same shape of idea as a remote ref (`remote/splitRemoteRef`):
9784
+ * a URL-ish string that **ends with the file's virtual path**, so the file's
9785
+ * logical identity survives wherever its bytes happen to be.
9786
+ *
9787
+ * /api/val/external/documents/f/{fileHash}/p/public/val/documents/report_a1b2c.pdf
9788
+ * \__________________________/ \________/ \_______________________________/
9789
+ * route + label content the path a LOCAL file
9790
+ * address would have had
9791
+ *
9792
+ * Three things follow from that shape, and each is load-bearing:
9793
+ *
9794
+ * - **`media.ts` needs no change.** `isRemote` is `!path.startsWith("/public")`,
9795
+ * and this does not, so `Internal.mediaUrl` returns the ref as-is and Val's
9796
+ * route serves it. The media rule that has been got wrong repeatedly is left
9797
+ * exactly alone.
9798
+ * - **Moving between storage modes is mechanical.** External → local reads the
9799
+ * `public/…` suffix back out, fetches the bytes and writes them there; local →
9800
+ * external uploads the file already at that path. Nothing is renamed.
9801
+ * - **The filename is Val's, not the adapter's**, produced by the same
9802
+ * `createFilename` a local or remote file uses — so the same bytes get the
9803
+ * same name under all three storage modes.
9804
+ */
9805
+
9806
+ var RegEx = /^\/api\/val\/external\/([^/]+)\/f\/([^/]+)\/p\/(.+)$/;
9807
+ function createExternalFileRef(_ref) {
9808
+ var label = _ref.label,
9809
+ fileHash = _ref.fileHash,
9810
+ filePath = _ref.filePath;
9811
+ var withoutLeadingSlash = filePath.startsWith("/") ? filePath.slice(1) : filePath;
9812
+ if (!withoutLeadingSlash.startsWith("public/")) {
9813
+ throw new Error("External file paths must be under '/public', got: '".concat(filePath, "'"));
9814
+ }
9815
+ return "/api/val/external/".concat(label, "/f/").concat(fileHash, "/p/").concat(withoutLeadingSlash);
9816
+ }
9817
+ function splitExternalFileRef(ref) {
9818
+ var match = ref.match(RegEx);
9819
+ if (!match) {
9820
+ return {
9821
+ status: "error",
9822
+ error: "Not an external file ref: " + ref
9823
+ };
9824
+ }
9825
+ var filePath = match[3];
9826
+ if (!filePath.startsWith("public/")) {
9827
+ return {
9828
+ status: "error",
9829
+ error: "Invalid external file ref: " + ref
9830
+ };
9831
+ }
9832
+ // Traversal check, for the same reason `splitRemoteRef` has one: this path is
9833
+ // used to read and write files.
9834
+ if (filePath.split("/").some(function (segment) {
9835
+ return segment === "." || segment === "..";
9836
+ })) {
9837
+ return {
9838
+ status: "error",
9839
+ error: "Invalid external file ref: " + ref
9840
+ };
9841
+ }
9842
+ return {
9843
+ status: "success",
9844
+ label: match[1],
9845
+ fileHash: match[2],
9846
+ filePath: filePath
9847
+ };
9848
+ }
9849
+ function isExternalFileRef(ref) {
9850
+ return RegEx.test(ref);
9851
+ }
9852
+
9496
9853
  // NOTE: the segments must match the ref built by createRemoteRef below and the RegEx in splitRemoteRef.
9497
9854
 
9498
9855
  function createRemoteRef(remoteHost, _ref) {
@@ -9553,7 +9910,7 @@ function deserializeSchema(serialized) {
9553
9910
  return schema;
9554
9911
  }
9555
9912
  function deserializeSchemaImpl(serialized) {
9556
- var _serialized$options, _serialized$options2, _serialized$render, _serialized$multiline, _serialized$render2, _serialized$render3, _serialized$render4, _serialized$render5, _serialized$render6, _serialized$render7, _serialized$accept, _serialized$directory, _serialized$remote, _serialized$jsonValue, _serialized$render9, _serialized$render0, _serialized$render10, _serialized$render11, _serialized$render12, _serialized$render13, _serialized$render14, _serialized$render15;
9913
+ var _serialized$options, _serialized$options2, _serialized$render, _serialized$multiline, _serialized$render2, _serialized$render3, _serialized$render4, _serialized$render5, _serialized$render6, _serialized$render7, _serialized$accept, _serialized$directory, _serialized$remote, _serialized$jsonValue, _serialized$render9, _serialized$external, _serialized$render0, _serialized$render10, _serialized$render11, _serialized$render12, _serialized$render13, _serialized$render14, _serialized$render15;
9557
9914
  switch (serialized.type) {
9558
9915
  case "string":
9559
9916
  return new StringSchema(_objectSpread2(_objectSpread2({}, serialized.options), {}, {
@@ -9614,7 +9971,10 @@ function deserializeSchemaImpl(serialized) {
9614
9971
  altSchema: serialized.alt ? deserializeSchema(serialized.alt) : undefined
9615
9972
  } : undefined, false, false, serialized.description, (_serialized$jsonValue = serialized.jsonValues) !== null && _serialized$jsonValue !== void 0 ? _serialized$jsonValue : false,
9616
9973
  // The preview closure cannot survive serialization; the render can.
9617
- null, (_serialized$render9 = serialized.render) !== null && _serialized$render9 !== void 0 ? _serialized$render9 : null);
9974
+ null, (_serialized$render9 = serialized.render) !== null && _serialized$render9 !== void 0 ? _serialized$render9 : null, // The external label is static data, so it survives serialization and
9975
+ // must be carried back: a deserialized schema that forgot it would walk
9976
+ // an external record as if its entries were local.
9977
+ (_serialized$external = serialized.external) !== null && _serialized$external !== void 0 ? _serialized$external : null);
9618
9978
  case "keyOf":
9619
9979
  return new KeyOfSchema(serialized.schema, serialized.path, serialized.opt, [], false, false, serialized.description, (_serialized$render0 = serialized.render) !== null && _serialized$render0 !== void 0 ? _serialized$render0 : null);
9620
9980
  case "route":
@@ -10176,6 +10536,11 @@ var Internal = {
10176
10536
  isJson: isJson,
10177
10537
  getJsonImport: getJsonImport,
10178
10538
  resolveJsonValues: resolveJsonValues,
10539
+ isExternal: isExternal,
10540
+ hasInlineExternalEntries: hasInlineExternalEntries,
10541
+ createExternalFileRef: createExternalFileRef,
10542
+ splitExternalFileRef: splitExternalFileRef,
10543
+ isExternalFileRef: isExternalFileRef,
10179
10544
  createValPathOfItem: createValPathOfItem,
10180
10545
  getSHA256Hash: getSHA256Hash,
10181
10546
  initSchema: initSchema,
@@ -10284,6 +10649,7 @@ exports.derefPatch = derefPatch;
10284
10649
  exports.deserializeSchema = deserializeSchema;
10285
10650
  exports.extractValModules = extractValModules;
10286
10651
  exports.getSourcePathFromRoute = getSourcePathFromRoute;
10652
+ exports.hasMediaSchema = hasMediaSchema;
10287
10653
  exports.hasRemoteFileSchema = hasRemoteFileSchema;
10288
10654
  exports.initVal = initVal;
10289
10655
  exports.isInlineRender = isInlineRender;