@plasius/schema 1.0.10 → 1.0.13

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.
@@ -2,8 +2,6 @@ name: CD (Publish to npm)
2
2
 
3
3
  on:
4
4
  workflow_dispatch:
5
- release:
6
- types: [published]
7
5
 
8
6
  permissions:
9
7
  contents: write
@@ -12,7 +10,7 @@ permissions:
12
10
  jobs:
13
11
  publish:
14
12
  runs-on: ubuntu-latest
15
- environment: npm
13
+ environment: production
16
14
  steps:
17
15
  - name: Checkout
18
16
  uses: actions/checkout@v4
@@ -40,6 +38,11 @@ jobs:
40
38
  NEW_VER=$(npm version patch -m "chore: release v%s [skip ci]")
41
39
  echo "New version: $NEW_VER"
42
40
  git push --follow-tags
41
+
42
+ # Expose tag (vX.Y.Z) and version (X.Y.Z) for later steps
43
+ VER_NO_V=${NEW_VER#v}
44
+ echo "tag=$NEW_VER" >> "$GITHUB_OUTPUT"
45
+ echo "version=$VER_NO_V" >> "$GITHUB_OUTPUT"
43
46
 
44
47
  NAME=$(node -p "require('./package.json').name")
45
48
  echo "name=$NAME" >> "$GITHUB_OUTPUT"
@@ -49,6 +52,104 @@ jobs:
49
52
  echo "flags=--access public" >> "$GITHUB_OUTPUT"
50
53
  fi
51
54
 
55
+ - name: Update CHANGELOG.md (move Unreleased to new version)
56
+ env:
57
+ VERSION: ${{ steps.pkg.outputs.version }}
58
+ TAG: ${{ steps.pkg.outputs.tag }}
59
+ GITHUB_REPOSITORY: ${{ github.repository }}
60
+ run: |
61
+ set -euo pipefail
62
+
63
+ FILE="CHANGELOG.md"
64
+ if [ ! -f "$FILE" ]; then
65
+ echo "No CHANGELOG.md found; skipping changelog update."
66
+ exit 0
67
+ fi
68
+
69
+ DATE=$(date -u +%Y-%m-%d)
70
+ VERSION_LINE="## [${VERSION}] - ${DATE}"
71
+
72
+ # Identify Unreleased block boundaries
73
+ UNREL_START=$(grep -n '^## \[Unreleased\]' "$FILE" | cut -d: -f1 || true)
74
+ if [ -z "$UNREL_START" ]; then
75
+ echo "No '## [Unreleased]' section found; skipping changelog update."
76
+ exit 0
77
+ fi
78
+ NEXT_HDR=$(awk 'NR>'"$UNREL_START"' && /^## \[/{print NR; exit}' "$FILE")
79
+ if [ -z "$NEXT_HDR" ]; then
80
+ NEXT_HDR=$(wc -l < "$FILE")
81
+ NEXT_HDR=$((NEXT_HDR+1))
82
+ fi
83
+
84
+ # Extract sections
85
+ HEADER=$(sed -n "1,${UNREL_START}p" "$FILE")
86
+ UNREL_CONTENT=$(sed -n "$((UNREL_START+1)),$((NEXT_HDR-1))p" "$FILE")
87
+ TAIL=$(sed -n "${NEXT_HDR},\$p" "$FILE")
88
+
89
+ # Prepare new Unreleased template (Keep a Changelog style) without tabs/indent issues
90
+ NEW_UNRELEASED=$(printf '%s\n' \
91
+ '## [Unreleased]' \
92
+ '- **Added**' \
93
+ ' - (placeholder)' \
94
+ '' \
95
+ '- **Changed**' \
96
+ ' - (placeholder)' \
97
+ '' \
98
+ '- **Fixed**' \
99
+ ' - (placeholder)' \
100
+ '' \
101
+ '- **Security**' \
102
+ ' - (placeholder)')
103
+
104
+ # Build the new CHANGELOG content
105
+ TMP_FILE=$(mktemp)
106
+ {
107
+ printf "%s\n" "$HEADER"
108
+ printf "%s\n\n" "$NEW_UNRELEASED"
109
+ printf "%s\n" "$VERSION_LINE"
110
+ # If Unreleased was empty, at least add a placeholder so the section isn't blank
111
+ if [ -z "$(echo "$UNREL_CONTENT" | tr -d '\n' | tr -d '[:space:]')" ]; then
112
+ printf "### Changed\n- (no notable changes)\n\n"
113
+ else
114
+ printf "%s\n" "$UNREL_CONTENT"
115
+ # Ensure a trailing newline after the inserted section
116
+ printf "\n"
117
+ fi
118
+ printf "%s\n" "$TAIL"
119
+ } > "$TMP_FILE"
120
+
121
+ mv "$TMP_FILE" "$FILE"
122
+
123
+ # Update bottom compare links
124
+ # Update [Unreleased] compare to start at v${VERSION}
125
+ COMPARE_URL="https://github.com/${GITHUB_REPOSITORY}/compare/v${VERSION}...HEAD"
126
+ sed -E -i.bak "s|^\[Unreleased\]: .*|[Unreleased]: ${COMPARE_URL}|" "$FILE" || true
127
+ rm -f "$FILE.bak"
128
+
129
+ # Append a link for the new version if not present
130
+ if ! grep -q "^\[${VERSION}\]:" "$FILE"; then
131
+ echo "[${VERSION}]: https://github.com/${GITHUB_REPOSITORY}/releases/tag/v${VERSION}" >> "$FILE"
132
+ fi
133
+
134
+ git add "$FILE"
135
+ git commit -m "docs(changelog): release v${VERSION}"
136
+ git push
137
+
138
+ - name: Create GitHub Release from tag (first-party)
139
+ env:
140
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
141
+ run: |
142
+ set -euo pipefail
143
+ TAG="${{ steps.pkg.outputs.tag }}"
144
+ if gh release view "$TAG" >/dev/null 2>&1; then
145
+ echo "Release $TAG already exists; skipping creation."
146
+ else
147
+ gh release create "$TAG" \
148
+ --title "Release $TAG" \
149
+ --generate-notes \
150
+ --latest
151
+ fi
152
+
52
153
  - name: Publish
53
154
  env:
54
155
  NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -6,7 +6,7 @@
6
6
  "request": "launch",
7
7
  "name": "Debug Vitest",
8
8
  "program": "${workspaceFolder}/node_modules/vitest/vitest.mjs",
9
- "args": ["run", "--dir", "tests"],
9
+ "args": ["run"],
10
10
  "cwd": "${workspaceFolder}",
11
11
  "console": "integratedTerminal",
12
12
  "skipFiles": ["<node_internals>/**"]
package/CHANGELOG.md CHANGED
@@ -1,5 +1,4 @@
1
1
 
2
-
3
2
  # Changelog
4
3
 
5
4
  All notable changes to this project will be documented in this file.
@@ -9,46 +8,63 @@ The format is based on **[Keep a Changelog](https://keepachangelog.com/en/1.1.0/
9
8
  ---
10
9
 
11
10
  ## [Unreleased]
12
- ### Added
13
- - (placeholder) Add new validators, field helpers, or PII utilities here.
11
+ ## [Unreleased]
12
+ - **Added**
13
+ - (placeholder)
14
+
15
+ - **Changed**
16
+ - (placeholder)
17
+
18
+ - **Fixed**
19
+ - (placeholder)
20
+
21
+ - **Security**
22
+ - (placeholder)
14
23
 
15
- ### Changed
16
- - (placeholder)
24
+ ## [1.0.13] - 2025-09-16
17
25
 
18
- ### Fixed
19
- - (placeholder)
26
+ - **Added**
27
+ - (placeholder) Add new validators, field helpers, or PII utilities here.
20
28
 
21
- ### Security
22
- - (placeholder)
29
+ - **Changed**
30
+ - ./src/schema.ts Added comments defining functionality on all externally facing functions.
31
+
32
+ - **Fixed**
33
+ - ./src/schema.ts Validation no longer mutates the input, internal system fields are set only on result if not previously present.
34
+
35
+ - **Security**
36
+ - (placeholder)
23
37
 
24
38
  ---
25
39
 
26
40
  ## [1.0.0] - 2025-09-16
27
- ### Added
28
- - Initial public release of `@plasius/schema`.
29
- - Fluent field builder API: `field().string().required()`, `field().number().min()`, etc.
30
- - Type inference utilities to derive TypeScript types from schema definitions.
31
- - Built-in validators for common standards:
32
- - ISO-3166 country codes
33
- - ISO-4217 currency codes
34
- - RFC 5322 email format
35
- - E.164 phone format
36
- - WHATWG URL format
37
- - ISO 8601 date/time
38
- - OWASP-guided text/name constraints
39
- - UUID (RFC 4122) and SemVer 2.0.0
40
- - PII annotations and helpers for redaction/masking before logging.
41
- - Lightweight validation runner with success/error result types.
42
-
43
- ### Changed
44
- - N/A (initial release)
45
-
46
- ### Fixed
47
- - N/A (initial release)
41
+
42
+ - **Added**
43
+ - Initial public release of `@plasius/schema`.
44
+ - Fluent field builder API: `field().string().required()`, `field().number().min()`, etc.
45
+ - Type inference utilities to derive TypeScript types from schema definitions.
46
+ - Built-in validators for common standards:
47
+ - ISO-3166 country codes
48
+ - ISO-4217 currency codes
49
+ - RFC 5322 email format
50
+ - E.164 phone format
51
+ - WHATWG URL format
52
+ - ISO 8601 date/time
53
+ - OWASP-guided text/name constraints
54
+ - UUID (RFC 4122) and SemVer 2.0.0
55
+ - PII annotations and helpers for redaction/masking before logging.
56
+ - Lightweight validation runner with success/error result types.
57
+
58
+ - **Changed**
59
+ - N/A (initial release)
60
+
61
+ - **Fixed**
62
+ - N/A (initial release)
48
63
 
49
64
  ---
50
65
 
51
66
  ## Release process (maintainers)
67
+
52
68
  1. Update `CHANGELOG.md` under **Unreleased** with user‑visible changes.
53
69
  2. Bump version in `package.json` following SemVer (major/minor/patch).
54
70
  3. Move entries from **Unreleased** to a new version section with the current date.
@@ -59,5 +75,6 @@ The format is based on **[Keep a Changelog](https://keepachangelog.com/en/1.1.0/
59
75
 
60
76
  ---
61
77
 
62
- [Unreleased]: https://github.com/Plasius-LTD/plasius-schema/compare/v1.0.0...HEAD
63
- [1.0.0]: https://github.com/Plasius-LTD/plasius-schema/releases/tag/v1.0.0
78
+ [Unreleased]: https://github.com/Plasius-LTD/schema/compare/v1.0.13...HEAD
79
+ [1.0.0]: https://github.com/Plasius-LTD/plasius-schema/releases/tag/v1.0.0
80
+ [1.0.13]: https://github.com/Plasius-LTD/schema/releases/tag/v1.0.13
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plasius/schema",
3
- "version": "1.0.10",
3
+ "version": "1.0.13",
4
4
  "description": "Entity schema definition & validation helpers for Plasius ecosystem",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
package/src/schema.ts CHANGED
@@ -500,15 +500,14 @@ export function createSchema<S extends SchemaShape>(
500
500
  if (typeof input !== "object" || input === null) {
501
501
  return { valid: false, errors: ["Input must be an object"] } as any;
502
502
  }
503
-
504
- if (!(input as any).type || !(input as any).version) {
505
- (input as any).type = entityType;
506
- (input as any).version = version;
507
- }
503
+ // Work on a non-mutating copy that includes system defaults for first-time objects
504
+ const working: Record<string, any> = { ...(input as any) };
505
+ if (working.type == null) working.type = entityType;
506
+ if (working.version == null) working.version = version;
508
507
 
509
508
  for (const key in schema._shape) {
510
509
  const def = schema._shape[key];
511
- const value = (input as any)[key];
510
+ const value = working[key];
512
511
 
513
512
  if (!def) {
514
513
  errors.push(`Field definition missing for: ${key}`);
@@ -553,6 +552,7 @@ export function createSchema<S extends SchemaShape>(
553
552
  result[key] = value;
554
553
  }
555
554
 
555
+
556
556
  if (errors.length === 0 && options.schemaValidator) {
557
557
  const castValue = result as Infer<S>;
558
558
  if (!options.schemaValidator(castValue)) {
@@ -570,7 +570,28 @@ export function createSchema<S extends SchemaShape>(
570
570
  // specific validator for a schema to allow conditional validation
571
571
  schemaValidator: options.schemaValidator!, // <== expose it here!
572
572
 
573
- // 🔗 Validate composition (references) recursively
573
+ /**
574
+ * Recursively validates entity references defined in this schema.
575
+ *
576
+ * Traverses fields of type `ref` and arrays of `ref` and resolves each target
577
+ * entity using the provided `resolveEntity` function. When `autoValidate` is
578
+ * enabled (default) and the field's `refPolicy` is `eager`, the referenced
579
+ * entity's schema is fetched and validated via `validateComposition` up to
580
+ * `maxDepth` levels.
581
+ *
582
+ * Skips fields not listed in `onlyFields` when provided. Prevents cycles via
583
+ * a `visited` set in `validatorContext`.
584
+ *
585
+ * @param entity The root entity to validate (must include `type` and `id`).
586
+ * @param options Options controlling traversal and resolution behavior.
587
+ * @param options.resolveEntity Function to resolve a referenced entity by type and id.
588
+ * @param options.validatorContext Internal context (visited set) to prevent cycles.
589
+ * @param options.maxDepth Maximum depth for recursive validation (default: 5).
590
+ * @param options.onlyFields Optional whitelist of field names to validate.
591
+ * @param options.log Optional logger for traversal/debug output.
592
+ *
593
+ * @throws Error if a broken reference is encountered (target cannot be resolved).
594
+ */
574
595
  async validateComposition(entity, options) {
575
596
  const {
576
597
  resolveEntity,
@@ -643,6 +664,11 @@ export function createSchema<S extends SchemaShape>(
643
664
  }
644
665
  },
645
666
 
667
+ /**
668
+ * Returns the configured table name for this schema.
669
+ *
670
+ * @throws Error if no store/table name has been defined for this schema.
671
+ */
646
672
  tableName(): string {
647
673
  if (!store || store === "") {
648
674
  throw new Error("Store is not defined for this schema");
@@ -650,7 +676,15 @@ export function createSchema<S extends SchemaShape>(
650
676
  return store;
651
677
  },
652
678
 
653
- // 🔒 Auto-prepare for storage (encrypt/hash PII)
679
+ /**
680
+ * Transforms an input object for persistence by applying PII protection
681
+ * according to field annotations (e.g., encryption and hashing).
682
+ *
683
+ * @param input The raw entity data.
684
+ * @param encryptFn Function used to encrypt sensitive values.
685
+ * @param hashFn Function used to hash sensitive values.
686
+ * @returns A new object safe to store.
687
+ */
654
688
  prepareForStorage(
655
689
  input: Record<string, any>,
656
690
  encryptFn: (value: any) => string,
@@ -659,6 +693,14 @@ export function createSchema<S extends SchemaShape>(
659
693
  return piiPrepareForStorage(_shape, input, encryptFn, hashFn);
660
694
  },
661
695
 
696
+ /**
697
+ * Reverses storage transformations for read paths (e.g., decrypts values)
698
+ * according to PII annotations, returning a consumer-friendly object.
699
+ *
700
+ * @param stored Data retrieved from storage.
701
+ * @param decryptFn Function used to decrypt values that were encrypted on write.
702
+ * @returns A new object suitable for application consumption.
703
+ */
662
704
  prepareForRead(
663
705
  stored: Record<string, any>,
664
706
  decryptFn: (value: string) => any
@@ -666,7 +708,14 @@ export function createSchema<S extends SchemaShape>(
666
708
  return piiPrepareForRead(_shape, stored, decryptFn);
667
709
  },
668
710
 
669
- // 🔍 Sanitize for logging (redact/pseudonymize PII)
711
+ /**
712
+ * Produces a log-safe copy of the provided data by redacting or pseudonymizing
713
+ * PII fields in accordance with field annotations.
714
+ *
715
+ * @param data Arbitrary data to sanitize for logging.
716
+ * @param pseudonymFn Function producing stable pseudonyms for sensitive values.
717
+ * @returns A copy safe to emit to logs.
718
+ */
670
719
  sanitizeForLog(
671
720
  data: Record<string, any>,
672
721
  pseudonymFn: (value: any) => string
@@ -674,6 +723,10 @@ export function createSchema<S extends SchemaShape>(
674
723
  return piiSanitizeForLog(_shape, data, pseudonymFn);
675
724
  },
676
725
 
726
+ /**
727
+ * Returns a list of fields annotated with PII metadata for auditing purposes.
728
+ * Each entry includes classification, required action, and optional log policy.
729
+ */
677
730
  getPiiAudit(): Array<{
678
731
  field: string;
679
732
  classification: PIIClassification;
@@ -684,10 +737,21 @@ export function createSchema<S extends SchemaShape>(
684
737
  return piiGetPiiAudit(_shape);
685
738
  },
686
739
 
740
+ /**
741
+ * Produces a copy of stored data suitable for data deletion flows by scrubbing
742
+ * or blanking PII per field annotations.
743
+ *
744
+ * @param stored Data as persisted.
745
+ * @returns A copy with PII removed or neutralized for deletion.
746
+ */
687
747
  scrubPiiForDelete(stored: Record<string, any>): Record<string, any> {
688
748
  return piiScrubPiiForDelete(_shape, stored);
689
749
  },
690
750
 
751
+ /**
752
+ * Returns a normalized description of the schema suitable for documentation
753
+ * or UI rendering (type, optionality, enum values, PII flags, etc.).
754
+ */
691
755
  describe() {
692
756
  const description: Record<string, any> = {};
693
757
  for (const [key, def] of Object.entries(schema._shape)) {
@@ -715,19 +779,24 @@ export function createSchema<S extends SchemaShape>(
715
779
  return schema;
716
780
  }
717
781
 
718
- // 🔗 Retrieve a previously registered schema globally
782
+ /**
783
+ * Retrieves a previously registered schema by its `entityType` from the
784
+ * in-process global schema registry.
785
+ */
719
786
  export function getSchemaForType(type: string): Schema<any> | undefined {
720
787
  return globalSchemaRegistry.get(type);
721
788
  }
722
789
 
723
- // 🔗 Retrieve all registered schemas globally
790
+ /**
791
+ * Returns all schemas registered in the in-process global registry.
792
+ */
724
793
  export function getAllSchemas(): Schema<any>[] {
725
794
  return Array.from(globalSchemaRegistry.values());
726
795
  }
727
796
 
728
797
  /**
729
- * Renders a schema description to a simplified frontend-consumable format.
730
- * This can be used in UIs for schema explorers, documentation, or admin tools.
798
+ * Renders a schema into a simplified descriptor for front-end consumption.
799
+ * Intended for documentation and admin tooling rather than validation.
731
800
  */
732
801
  export function renderSchemaDescription(
733
802
  schema: Schema<any>