@powerlines/schema 0.11.75 → 0.11.77

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/index.mjs CHANGED
@@ -2,7 +2,7 @@ import { createUnpluginResolver } from "@powerlines/core/lib/unplugin";
2
2
  import { resolveOptions } from "@powerlines/unplugin/esbuild";
3
3
  import { omit } from "@stryke/helpers/omit";
4
4
  import { findFileName } from "@stryke/path/file-path-fns";
5
- import defu, { defu as defu$1 } from "defu";
5
+ import defu from "defu";
6
6
  import { build } from "esbuild";
7
7
  import { createEsbuildPlugin } from "unplugin";
8
8
  import { toBool } from "@stryke/convert/to-bool";
@@ -173,10 +173,9 @@ function getPrimarySchemaType(schema) {
173
173
  //#endregion
174
174
  //#region src/type-checks.ts
175
175
  const isSetNumber = (value) => typeof value === "number" && Number.isFinite(value);
176
- const isSetBoolean = (value) => typeof value === "boolean";
177
- const isSchemaLikeValue = (value) => isSetObject(value) || isSetBoolean(value);
176
+ const isSchemaLikeValue = (value) => isSetObject(value) || isBoolean(value);
178
177
  const isRecordOfSchemaLike = (value) => isSetObject(value) && Object.values(value).every((item) => isSchemaLikeValue(item));
179
- const isVocabularyMap = (value) => isSetObject(value) && Object.values(value).every((item) => isSetBoolean(item));
178
+ const isVocabularyMap = (value) => isSetObject(value) && Object.values(value).every((item) => isBoolean(item));
180
179
  const isStringArray = (value) => Array.isArray(value) && value.every((item) => isSetString(item));
181
180
  const isRecordOfStringArrays = (value) => isSetObject(value) && Object.values(value).every((item) => isStringArray(item));
182
181
  const JSON_SCHEMA_PRIMITIVE_TYPE_SET = new Set(JSON_SCHEMA_PRIMITIVE_TYPES);
@@ -238,8 +237,8 @@ const isUntypedFunctionArg = (value) => {
238
237
  const isRecordOfUntypedSchema = (value) => isSetObject(value) && Object.values(value).every((item) => isUntypedSchema(item));
239
238
  const isArrayOf = (value, predicate) => Array.isArray(value) && value.every((item) => predicate(item));
240
239
  const isTupleOfTwo = (value, aPredicate, bPredicate) => Array.isArray(value) && value.length === 2 && aPredicate(value[0]) && bPredicate(value[1]);
241
- const isOptionalString = (value) => value === void 0 || isSetString(value);
242
- const isOptionalBoolean = (value) => value === void 0 || isSetBoolean(value);
240
+ const isOptionalString = (value, allowEmpty = false) => value === void 0 || isString(value) && (allowEmpty || value.length > 0);
241
+ const isOptionalBoolean = (value) => value === void 0 || isBoolean(value);
243
242
  const isOptionalNumber = (value) => value === void 0 || isSetNumber(value);
244
243
  const isOptionalBigint = (value) => value === void 0 || isSetBigint(value);
245
244
  const isOptionalJsonSchema = (value) => value === void 0 || isJsonSchema(value);
@@ -250,15 +249,15 @@ const hasValidJsonSchemaKeywords = (schema) => {
250
249
  if (!isOptionalString(schema.$id)) return false;
251
250
  if (!isOptionalString(schema.$schema)) return false;
252
251
  if (schema.$vocabulary !== void 0 && !isVocabularyMap(schema.$vocabulary)) return false;
253
- if (!isOptionalString(schema.$comment)) return false;
252
+ if (!isOptionalString(schema.$comment, true)) return false;
254
253
  if (!isOptionalString(schema.$anchor)) return false;
255
254
  if (schema.$defs !== void 0 && !isRecordOfSchemaLike(schema.$defs)) return false;
256
255
  if (!isOptionalString(schema.$dynamicRef)) return false;
257
256
  if (!isOptionalString(schema.$dynamicAnchor)) return false;
258
257
  if (!isOptionalString(schema.name)) return false;
259
- if (!isOptionalString(schema.title)) return false;
260
- if (!isOptionalString(schema.description)) return false;
261
- if (!isOptionalString(schema.docs)) return false;
258
+ if (!isOptionalString(schema.title, true)) return false;
259
+ if (!isOptionalString(schema.description, true)) return false;
260
+ if (!isOptionalString(schema.docs, true)) return false;
262
261
  if (schema.examples !== void 0 && !Array.isArray(schema.examples)) return false;
263
262
  if (schema.alias !== void 0 && !isStringArray(schema.alias)) return false;
264
263
  if (schema.tags !== void 0 && !isStringArray(schema.tags)) return false;
@@ -309,7 +308,7 @@ function isJsonSchemaArray(input) {
309
308
  if (!isSetObject(input)) return false;
310
309
  const schema = input;
311
310
  if (!hasValidJsonSchemaKeywords(schema) || schema.type !== "array") return false;
312
- return isOptionalJsonSchemaArray(schema.prefixItems) && isOptionalJsonSchema(schema.items) && isOptionalJsonSchema(schema.contains) && isOptionalNumber(schema.minItems) && isOptionalNumber(schema.maxItems) && isOptionalBoolean(schema.uniqueItems) && isOptionalNumber(schema.minContains) && isOptionalNumber(schema.maxContains) && (schema.unevaluatedItems === void 0 || isSetBoolean(schema.unevaluatedItems) || isJsonSchema(schema.unevaluatedItems));
311
+ return isOptionalJsonSchemaArray(schema.prefixItems) && isOptionalJsonSchema(schema.items) && isOptionalJsonSchema(schema.contains) && isOptionalNumber(schema.minItems) && isOptionalNumber(schema.maxItems) && isOptionalBoolean(schema.uniqueItems) && isOptionalNumber(schema.minContains) && isOptionalNumber(schema.maxContains) && (schema.unevaluatedItems === void 0 || isBoolean(schema.unevaluatedItems) || isJsonSchema(schema.unevaluatedItems));
313
312
  }
314
313
  /**
315
314
  * Type guard for bigint-backed integer schemas.
@@ -364,7 +363,7 @@ function isJsonSchemaEnum(input) {
364
363
  const defaultValue = schema.default;
365
364
  if (typeName === "string") return enumValues.every((value) => isSetString(value)) && (defaultValue === void 0 || isSetString(defaultValue));
366
365
  if (typeName === "number" || typeName === "integer") return enumValues.every((value) => isSetNumber(value)) && (defaultValue === void 0 || isSetNumber(defaultValue));
367
- if (typeName === "boolean") return enumValues.every((value) => isSetBoolean(value)) && (defaultValue === void 0 || isSetBoolean(defaultValue));
366
+ if (typeName === "boolean") return enumValues.every((value) => isBoolean(value)) && (defaultValue === void 0 || isBoolean(defaultValue));
368
367
  return typeName === "null" && enumValues.every((value) => value === null) && (defaultValue === void 0 || defaultValue === null);
369
368
  }
370
369
  /**
@@ -377,7 +376,7 @@ function isJsonSchemaAllOf(input) {
377
376
  if (!isSetObject(input)) return false;
378
377
  const schema = input;
379
378
  if (!hasValidJsonSchemaKeywords(schema) || !isArrayOf(schema.allOf, isJsonSchema)) return false;
380
- return schema.unevaluatedProperties === void 0 || isSetBoolean(schema.unevaluatedProperties) || isJsonSchema(schema.unevaluatedProperties);
379
+ return schema.unevaluatedProperties === void 0 || isBoolean(schema.unevaluatedProperties) || isJsonSchema(schema.unevaluatedProperties);
381
380
  }
382
381
  /**
383
382
  * Type guard for literal-value schemas.
@@ -491,7 +490,7 @@ function isJsonSchemaDecimal(input) {
491
490
  function isJsonSchemaObject(input) {
492
491
  if (!isSetObject(input)) return false;
493
492
  const schema = input;
494
- return hasValidJsonSchemaKeywords(schema) && schema.type === "object" && (schema.properties === void 0 || isRecordOfSchemaLike(schema.properties)) && (schema.patternProperties === void 0 || isRecordOfSchemaLike(schema.patternProperties)) && (schema.additionalProperties === void 0 || isSetBoolean(schema.additionalProperties) || isJsonSchema(schema.additionalProperties)) && (schema.required === void 0 || isStringArray(schema.required)) && (schema.unevaluatedProperties === void 0 || isSetBoolean(schema.unevaluatedProperties) || isJsonSchema(schema.unevaluatedProperties)) && (schema.dependencies === void 0 || isSetObject(schema.dependencies) && Object.values(schema.dependencies).every((item) => isStringArray(item) || isJsonSchema(item))) && (schema.dependentRequired === void 0 || isRecordOfStringArrays(schema.dependentRequired)) && (schema.dependentSchemas === void 0 || isRecordOfSchemaLike(schema.dependentSchemas)) && isOptionalNumber(schema.minProperties) && isOptionalNumber(schema.maxProperties) && (schema.primaryKey === void 0 || isStringArray(schema.primaryKey)) && isOptionalString(schema.databaseSchema);
493
+ return hasValidJsonSchemaKeywords(schema) && schema.type === "object" && (schema.properties === void 0 || isRecordOfSchemaLike(schema.properties)) && (schema.patternProperties === void 0 || isRecordOfSchemaLike(schema.patternProperties)) && (schema.additionalProperties === void 0 || isBoolean(schema.additionalProperties) || isJsonSchema(schema.additionalProperties)) && (schema.required === void 0 || isStringArray(schema.required)) && (schema.unevaluatedProperties === void 0 || isBoolean(schema.unevaluatedProperties) || isJsonSchema(schema.unevaluatedProperties)) && (schema.dependencies === void 0 || isSetObject(schema.dependencies) && Object.values(schema.dependencies).every((item) => isStringArray(item) || isJsonSchema(item))) && (schema.dependentRequired === void 0 || isRecordOfStringArrays(schema.dependentRequired)) && (schema.dependentSchemas === void 0 || isRecordOfSchemaLike(schema.dependentSchemas)) && isOptionalNumber(schema.minProperties) && isOptionalNumber(schema.maxProperties) && (schema.primaryKey === void 0 || isStringArray(schema.primaryKey)) && isOptionalString(schema.databaseSchema);
495
494
  }
496
495
  /**
497
496
  * Type guard for string schemas.
@@ -513,7 +512,7 @@ function isJsonSchemaString(input) {
513
512
  function isJsonSchemaSet(input) {
514
513
  if (!isSetObject(input)) return false;
515
514
  const schema = input;
516
- return hasValidJsonSchemaKeywords(schema) && schema.type === "array" && schema.uniqueItems === true && isOptionalJsonSchemaArray(schema.prefixItems) && isOptionalJsonSchema(schema.items) && isOptionalJsonSchema(schema.contains) && isOptionalNumber(schema.minItems) && isOptionalNumber(schema.maxItems) && isOptionalNumber(schema.minContains) && isOptionalNumber(schema.maxContains) && (schema.unevaluatedItems === void 0 || isSetBoolean(schema.unevaluatedItems) || isJsonSchema(schema.unevaluatedItems));
515
+ return hasValidJsonSchemaKeywords(schema) && schema.type === "array" && schema.uniqueItems === true && isOptionalJsonSchemaArray(schema.prefixItems) && isOptionalJsonSchema(schema.items) && isOptionalJsonSchema(schema.contains) && isOptionalNumber(schema.minItems) && isOptionalNumber(schema.maxItems) && isOptionalNumber(schema.minContains) && isOptionalNumber(schema.maxContains) && (schema.unevaluatedItems === void 0 || isBoolean(schema.unevaluatedItems) || isJsonSchema(schema.unevaluatedItems));
517
516
  }
518
517
  /**
519
518
  * Type guard for record schemas.
@@ -524,7 +523,7 @@ function isJsonSchemaSet(input) {
524
523
  function isJsonSchemaRecord(input) {
525
524
  if (!isSetObject(input)) return false;
526
525
  const schema = input;
527
- return hasValidJsonSchemaKeywords(schema) && schema.type === "object" && (schema.patternProperties === void 0 || isRecordOfSchemaLike(schema.patternProperties)) && (schema.additionalProperties === void 0 || isSetBoolean(schema.additionalProperties) || isJsonSchema(schema.additionalProperties)) && isOptionalJsonSchema(schema.propertyNames);
526
+ return hasValidJsonSchemaKeywords(schema) && schema.type === "object" && (schema.patternProperties === void 0 || isRecordOfSchemaLike(schema.patternProperties)) && (schema.additionalProperties === void 0 || isBoolean(schema.additionalProperties) || isJsonSchema(schema.additionalProperties)) && isOptionalJsonSchema(schema.propertyNames);
528
527
  }
529
528
  /**
530
529
  * Type guard for tuple schemas.
@@ -535,7 +534,7 @@ function isJsonSchemaRecord(input) {
535
534
  function isJsonSchemaTuple(input) {
536
535
  if (!isSetObject(input)) return false;
537
536
  const schema = input;
538
- return hasValidJsonSchemaKeywords(schema) && schema.type === "array" && isArrayOf(schema.prefixItems, isJsonSchema) && isOptionalNumber(schema.minItems) && isOptionalNumber(schema.maxItems) && isOptionalJsonSchema(schema.items) && isOptionalJsonSchema(schema.contains) && isOptionalBoolean(schema.uniqueItems) && isOptionalNumber(schema.minContains) && isOptionalNumber(schema.maxContains) && (schema.unevaluatedItems === void 0 || isSetBoolean(schema.unevaluatedItems) || isJsonSchema(schema.unevaluatedItems));
537
+ return hasValidJsonSchemaKeywords(schema) && schema.type === "array" && isArrayOf(schema.prefixItems, isJsonSchema) && isOptionalNumber(schema.minItems) && isOptionalNumber(schema.maxItems) && isOptionalJsonSchema(schema.items) && isOptionalJsonSchema(schema.contains) && isOptionalBoolean(schema.uniqueItems) && isOptionalNumber(schema.minContains) && isOptionalNumber(schema.maxContains) && (schema.unevaluatedItems === void 0 || isBoolean(schema.unevaluatedItems) || isJsonSchema(schema.unevaluatedItems));
539
538
  }
540
539
  /**
541
540
  * Type guard for undefined-representing schemas.
@@ -563,7 +562,7 @@ function isJsonSchemaPrimitiveUnion(input) {
563
562
  if (!Array.isArray(schema.enum)) return false;
564
563
  if (schema.type === "string") return schema.enum.every((value) => isSetString(value)) && (schema.default === void 0 || isSetString(schema.default));
565
564
  if (schema.type === "number") return schema.enum.every((value) => isSetNumber(value)) && (schema.default === void 0 || isSetNumber(schema.default));
566
- if (schema.type === "boolean") return schema.enum.every((value) => isSetBoolean(value)) && (schema.default === void 0 || isSetBoolean(schema.default));
565
+ if (schema.type === "boolean") return schema.enum.every((value) => isBoolean(value)) && (schema.default === void 0 || isBoolean(schema.default));
567
566
  if (schema.type === "integer") {
568
567
  if (schema.format !== "int64") return false;
569
568
  return schema.enum.every((value) => isSetBigint(value)) && (schema.default === void 0 || isSetBigint(schema.default));
@@ -636,7 +635,7 @@ function isStandardSchema(input) {
636
635
  function isValibotSchema(input) {
637
636
  if (!isSetObject(input) || !isStandardSchema(input)) return false;
638
637
  const schema = input;
639
- return schema.kind === "schema" && isSetString(schema.type) && isSetBoolean(schema.async) && isFunction(schema.reference) && isSetString(schema.expects) && isFunction(schema["~run"]);
638
+ return schema.kind === "schema" && isSetString(schema.type) && isBoolean(schema.async) && isFunction(schema.reference) && isSetString(schema.expects) && isFunction(schema["~run"]);
640
639
  }
641
640
  /**
642
641
  * Type guard for JSON Schema types.
@@ -774,7 +773,7 @@ function isSchemaObject(input) {
774
773
  */
775
774
  function getJsonSchema(input) {
776
775
  const schema = isSchema(input) ? input.schema : input;
777
- if (!isJsonSchema(schema)) throw new TypeError(`The provided input is not a valid JSON Schema: ${JSON.stringify(schema, null, 2)}`);
776
+ if (!isJsonSchema(schema)) throw new TypeError(`The provided input is not a valid JSON Schema`);
778
777
  return schema;
779
778
  }
780
779
  /**
@@ -789,7 +788,7 @@ function getJsonSchema(input) {
789
788
  */
790
789
  function getJsonSchemaObject(input) {
791
790
  const schema = getJsonSchema(input);
792
- if (!isJsonSchemaObject(schema)) throw new TypeError(`The provided input is not a valid JSON Schema object: ${JSON.stringify(schema, null, 2)}`);
791
+ if (!isJsonSchemaObject(schema)) throw new TypeError(`The provided input is not a valid JSON Schema object`);
793
792
  return schema;
794
793
  }
795
794
  /**
@@ -852,21 +851,100 @@ function addProperty(obj, name, property) {
852
851
  if (schema.required.length === 0) delete schema.required;
853
852
  }
854
853
  /**
854
+ * Keywords whose values are a flat record of named JSON Schema fragments.
855
+ * Each child schema is merged recursively with its counterpart.
856
+ */
857
+ const SCHEMA_RECORD_KEYWORDS = new Set([
858
+ "properties",
859
+ "patternProperties",
860
+ "$defs",
861
+ "definitions",
862
+ "dependentSchemas"
863
+ ]);
864
+ /**
865
+ * Keywords whose value is a single JSON Schema fragment that should be
866
+ * recursively merged when both sides define it.
867
+ */
868
+ const SCHEMA_SINGLE_KEYWORDS = new Set([
869
+ "if",
870
+ "then",
871
+ "else",
872
+ "not",
873
+ "contains",
874
+ "items",
875
+ "additionalProperties",
876
+ "unevaluatedProperties",
877
+ "propertyNames",
878
+ "unevaluatedItems"
879
+ ]);
880
+ /**
881
+ * Keywords whose values are arrays of JSON Schema fragments that should be
882
+ * concatenated (rather than overridden) during a merge.
883
+ */
884
+ const SCHEMA_ARRAY_CONCAT_KEYWORDS = new Set([
885
+ "allOf",
886
+ "anyOf",
887
+ "oneOf"
888
+ ]);
889
+ /**
890
+ * Recursively merges two JSON Schema fragments. `override` wins for any scalar
891
+ * key that both schemas define, while structured keywords are handled
892
+ * specially:
893
+ *
894
+ * - `properties`, `patternProperties`, `$defs`, `definitions`,
895
+ * `dependentSchemas` — each matching child schema is merged recursively.
896
+ * - `allOf`, `anyOf`, `oneOf` — arrays are concatenated.
897
+ * - `if`, `then`, `else`, `not`, `contains`, `items`,
898
+ * `additionalProperties`, `unevaluatedProperties`, `propertyNames`,
899
+ * `unevaluatedItems` — merged recursively when both sides are schemas.
900
+ * - `required` — arrays are unioned and deduplicated.
901
+ */
902
+ function mergeTwo(base, override) {
903
+ const result = { ...base };
904
+ for (const [key, overrideValue] of Object.entries(override)) {
905
+ const baseValue = result[key];
906
+ if (key === "required") result[key] = getUnique([...Array.isArray(baseValue) ? baseValue : [], ...Array.isArray(overrideValue) ? overrideValue : []]);
907
+ else if (SCHEMA_RECORD_KEYWORDS.has(key) && isSetObject(baseValue) && isSetObject(overrideValue)) {
908
+ const merged = { ...baseValue };
909
+ for (const [childKey, childOverride] of Object.entries(overrideValue)) {
910
+ const childBase = merged[childKey];
911
+ merged[childKey] = isJsonSchema(childBase) && isJsonSchema(childOverride) ? mergeTwo(childBase, childOverride) : childOverride;
912
+ }
913
+ result[key] = merged;
914
+ } else if (SCHEMA_ARRAY_CONCAT_KEYWORDS.has(key) && Array.isArray(baseValue) && Array.isArray(overrideValue)) result[key] = [...baseValue, ...overrideValue];
915
+ else if (SCHEMA_SINGLE_KEYWORDS.has(key) && isJsonSchema(baseValue) && isJsonSchema(overrideValue)) result[key] = mergeTwo(baseValue, overrideValue);
916
+ else result[key] = overrideValue;
917
+ }
918
+ return result;
919
+ }
920
+ /**
855
921
  * Merges multiple JSON Schemas into one.
856
922
  *
857
923
  * @remarks
858
- * This function takes multiple JSON Schemas or Schema wrappers and merges them into a single JSON Schema object. The merging process combines properties and metadata from all provided schemas, with later schemas in the arguments list taking precedence over earlier ones in case of conflicts. The resulting schema will include all unique properties and metadata from the input schemas.
924
+ * This function takes multiple JSON Schemas or Schema wrappers and merges them
925
+ * into a single JSON Schema object. Later schemas in the argument list take
926
+ * precedence over earlier ones for scalar conflicts.
927
+ *
928
+ * Structured keywords are merged recursively:
929
+ * - Named child schemas (`properties`, `$defs`, etc.) are merged
930
+ * per-property via recursive calls to `merge`.
931
+ * - Composition arrays (`allOf`, `anyOf`, `oneOf`) are concatenated.
932
+ * - Single-schema keywords (`if`, `then`, `else`, `not`, `items`, etc.)
933
+ * are merged recursively when both sides define them.
934
+ * - `required` arrays are unioned and deduplicated.
859
935
  *
860
936
  * @param schemas - An array of JSON Schemas or Schema wrappers to merge.
861
937
  * @returns A new JSON Schema that is the result of merging all input schemas.
862
938
  */
863
939
  function merge(...schemas) {
864
- let result = {};
865
- for (const schema of schemas.reverse()) if (!result.type || result.type === schema.type) {
866
- result = defu$1(result, getJsonSchema(schema));
867
- if (isJsonSchemaObject(result)) result.required = getUnique(result.required ?? []);
868
- }
869
- return result;
940
+ const jsonSchemas = schemas.map((s) => getJsonSchema(s));
941
+ if (jsonSchemas.length === 0) return {};
942
+ return jsonSchemas.reduce((acc, schema) => {
943
+ const accType = acc.type;
944
+ const schemaType = schema.type;
945
+ if (accType && schemaType && accType !== schemaType) return schema;
946
+ return mergeTwo(acc, schema);
947
+ });
870
948
  }
871
949
  /**
872
950
  * Returns whether a JSON Schema fragment accepts `null`.