@asaidimu/anansi 4.0.2 → 8.6.2

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/README.md CHANGED
@@ -1,739 +1,258 @@
1
- # Anansi: A Schema-Driven Data Modeling Toolkit
1
+ # @asaidimu/anansi
2
2
 
3
- **Anansi** is a comprehensive TypeScript toolkit for defining, versioning, migrating, and persisting structured data, enabling schema-driven development with powerful runtime validation and adaptable storage layers.
3
+ Self-contained TypeScript implementation of the **Anansi binary wire format**
4
+ the schema-driven, high-performance serialization used by the Go-Anansi
5
+ persistence layer. Compile schemas, address fields, encode/decode packets, and
6
+ validate documents — all in TypeScript, byte-compatible with the Go
7
+ implementation.
4
8
 
5
- ![npm version](https://img.shields.io/npm/v/@asaidimu/anansi?style=flat-square)
6
- ![License](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)
7
- ![Build Status](https://img.shields.io/badge/Build-Passing-brightgreen?style=flat-square)
9
+ > **Versioning**: released in tandem with the Go library. v8.x.y of this
10
+ > package is wire-compatible with go-anansi v8.x.y.
8
11
 
9
- ---
12
+ ## Install
10
13
 
11
- ### Table of Contents
12
-
13
- 1. [Overview & Features](#overview--features)
14
- 2. [Installation & Setup](#installation--setup)
15
- 3. [Usage Documentation](#usage-documentation)
16
- * [Core Concepts](#core-concepts)
17
- * [Defining Schemas](#defining-schemas)
18
- * [Schema Registry](#schema-registry)
19
- * [Schema Evolution (Migrations)](#schema-evolution-migrations)
20
- * [Runtime Validation](#runtime-validation)
21
- * [Developer Tools](#developer-tools)
22
- 4. [Project Architecture](#project-architecture)
23
- * [Core Components](#core-components)
24
- * [Extension Points](#extension-points)
25
- 5. [Development & Contributing](#development--contributing)
26
- * [Local Development Setup](#local-development-setup)
27
- * [Available Scripts](#available-scripts)
28
- * [Testing](#testing)
29
- * [Contributing Guidelines](#contributing-guidelines)
30
- * [Issue Reporting](#issue-reporting)
31
- 6. [Additional Information](#additional-information)
32
- * [Troubleshooting](#troubleshooting)
33
- * [Changelog & Roadmap](#changelog--roadmap)
34
- * [License](#license)
35
- * [Acknowledgments](#acknowledgments)
36
-
37
- ---
38
-
39
- ## Overview & Features
40
-
41
- Anansi is designed to streamline data management in TypeScript applications by providing a robust and flexible framework for defining data models, ensuring data integrity, and managing schema evolution over time. It abstracts away the complexities of various persistence layers, allowing developers to focus on the business logic while maintaining strong type safety and consistency.
42
-
43
- Whether you're building a new application from scratch or need to bring order to an existing, evolving data landscape, Anansi offers the tools to define clear, verifiable data structures, automate schema updates, and integrate seamlessly with diverse storage solutions. Its modular design promotes extensibility, allowing for easy integration of new features and custom adapters.
44
-
45
- ### Key Features
46
-
47
- * **Declarative Schema Definition**: Define complex data models using a rich `SchemaDefinition` interface, supporting primitive types, arrays, sets, enums, objects, discriminated unions, and relationships. Includes support for custom constraints, indexes, and descriptive metadata.
48
- * **Comprehensive Versioning & Migration**: Manage schema evolution gracefully with a built-in migration engine. Define schema changes and data transformation functions to automatically upgrade or rollback data between versions, ensuring data consistency across deployments.
49
- * **Pluggable Persistence Layer**: Interact with various data storage backends through a unified `Persistence` API. Comes with an in-memory ephemeral store for rapid prototyping and easily extendable to support other databases.
50
- * **Runtime Data Validation**: Automatically generate TypeScript validators from your `SchemaDefinition` to enforce data integrity at runtime. Integrates with standard validation specifications (`@standard-schema/spec`) and can be adapted for popular form libraries like React Hook Form.
51
- * **Git-Powered Schema Registry**: Store and manage your `SchemaDefinition` files in a version-controlled, collaborative registry. The Git integration (`isomorphic-git` and `LightningFS`) allows for distributed schema management, branching, tagging, and synchronization with remote repositories like GitHub or Gitea.
52
- * **Eventing & Observability**: Hook into persistence operations with a powerful event bus. Register triggers and schedule tasks to automate workflows, react to data changes, or perform maintenance operations. Access comprehensive metadata for monitoring your collections.
53
- * **Developer Tooling**: Boost productivity with utility functions for generating TypeScript types directly from your schemas, creating human-readable Markdown documentation, and applying JSON Patch operations for granular data updates.
54
-
55
- ## Installation & Setup
56
-
57
- ### Prerequisites
58
-
59
- To use Anansi, ensure you have the following installed:
60
-
61
- * **Node.js**: v18.x or higher
62
- * **Bun**: (Recommended for running scripts and faster dependency installation) or `npm`/`yarn`
63
- * **TypeScript**: v5.x or higher
14
+ ```sh
15
+ bun add @asaidimu/anansi # or npm/pnpm/yarn
16
+ ```
64
17
 
65
- ### Installation Steps
18
+ Runs everywhere: Bun, Node ≥ 18, and browsers (WebCrypto + WASM backends; no
19
+ Node built-ins in the codec paths).
66
20
 
67
- Install Anansi in your project using Bun (recommended) or your preferred package manager:
21
+ ## Quick start
68
22
 
69
- ```bash
70
- # Using Bun
71
- bun add @asaidimu/anansi
23
+ ```ts
24
+ import { AnansiCodec } from "@asaidimu/anansi";
72
25
 
73
- # Using npm
74
- npm install @asaidimu/anansi
26
+ // Compile once (per schema version / endpoint), cache the instance.
27
+ const codec = await AnansiCodec.create(schemaJSON, { fullVersion: 7 });
75
28
 
76
- # Using yarn
77
- yarn add @asaidimu/anansi
29
+ // Encode & decode documents — auto-selects Dense vs Sparse framing.
30
+ const wire = await codec.encode(order);
31
+ const { version, doc } = await codec.decode(wire);
78
32
  ```
79
33
 
80
- ### Configuration
81
-
82
- Anansi is designed to be highly configurable through its API. For internal development, modules are often aliased with `@core` to `src/`. If you are importing from the distributed package, you'll use `@asaidimu/anansi`.
34
+ The codec binds everything expensive: compiled tables, addressing, and your
35
+ transform/key choices. Instances are immutable — share one across requests,
36
+ or keep one per endpoint/collection at your discretion.
83
37
 
84
- No global configuration files are strictly required, but specific persistence adapters will require their own configuration (e.g., API URLs, authentication tokens).
38
+ ### Batches
85
39
 
86
- ### Verification
40
+ ```ts
41
+ const upload = await codec.encodeBatch(orders); // row-oriented
42
+ const results = await codec.decodeBatch(serverBytes); // row or columnar in
43
+ ```
87
44
 
88
- You can quickly verify the installation by trying a simple import in a TypeScript file:
45
+ ## Client-side integration
46
+
47
+ The package ships the **codec only** — transport is yours. A typical web app
48
+ wires it into `fetch` like this:
49
+
50
+ **1. Bootstrap: fetch the schema once, compile once, cache per version.**
51
+
52
+ ```ts
53
+ // anansi-client.ts
54
+ import {
55
+ AnansiCodec,
56
+ DocumentValidator, metaSchemaPredicateMap,
57
+ } from "@asaidimu/anansi";
58
+
59
+ export class AnansiClient {
60
+ private constructor(
61
+ private baseUrl: string,
62
+ private codec: AnansiCodec,
63
+ private validator: DocumentValidator,
64
+ ) {}
65
+
66
+ /** Server exposes its schema JSON + active version (once per session). */
67
+ static async connect(baseUrl: string): Promise<AnansiClient> {
68
+ const res = await fetch(`${baseUrl}/schema`);
69
+ const { schema, fullVersion } = await res.json();
70
+
71
+ const codec = await AnansiCodec.create(schema, { fullVersion });
72
+ const validator = await DocumentValidator.create(
73
+ schema as never, metaSchemaPredicateMap,
74
+ );
75
+ return new AnansiClient(baseUrl, codec, validator);
76
+ }
89
77
 
90
- ```typescript
91
- import { createEphemeralPersistence } from '@asaidimu/anansi';
92
- import type { SchemaDefinition } from '@asaidimu/anansi';
78
+ /** Send one document; returns the server's decoded reply. */
79
+ async send(path: string, doc: Record<string, unknown>) {
80
+ // Optional but recommended: validate user input before encoding.
81
+ const issues = await this.validator.validate(doc);
82
+ if (issues.length) throw new Error(`invalid document: ${issues[0]!.code}`);
93
83
 
94
- // Define a simple schema
95
- const userSchema: SchemaDefinition = {
96
- name: 'User',
97
- version: '1.0.0',
98
- fields: {
99
- id: { name: 'id', type: 'string', required: true },
100
- name: { name: 'name', type: 'string' }
101
- },
102
- nestedSchemas: {} // Always required, even if empty
103
- };
84
+ const wire = await this.codec.encode(doc);
104
85
 
105
- // Create an in-memory persistence instance
106
- const persistence = createEphemeralPersistence({}, {}); // Requires functionMap and predicateMap (can be empty for basic usage)
86
+ const res = await fetch(this.baseUrl + path, {
87
+ method: "POST",
88
+ headers: {
89
+ "Content-Type": "application/vnd.anansi.binary",
90
+ "X-Anansi-Version": String(this.codec.fullVersion),
91
+ },
92
+ body: wire,
93
+ });
107
94
 
108
- async function verify() {
109
- try {
110
- const userCollection = await persistence.create<typeof userSchema>({ schema: userSchema });
111
- console.log(`Collection '${userCollection.schema().name}' created successfully.`);
112
- const collections = await persistence.collections();
113
- console.log('Available collections:', collections);
114
- } catch (error) {
115
- console.error('Verification failed:', error);
95
+ return this.codec.decode(new Uint8Array(await res.arrayBuffer()));
116
96
  }
117
97
  }
118
-
119
- verify();
120
98
  ```
121
99
 
122
- ## Usage Documentation
123
-
124
- ### Core Concepts
100
+ **2. Use it like any API client.**
125
101
 
126
- Anansi revolves around several core concepts:
127
-
128
- * **`SchemaDefinition`**: The blueprint for your data. It describes fields, their types, constraints, indexes, and nested structures.
129
- * **`Persistence`**: The high-level interface for interacting with data stores (e.g., creating collections, managing schemas globally).
130
- * **`PersistenceCollection`**: An instance tied to a specific schema/collection, providing CRUD, validation, and migration operations for that data.
131
- * **`Migration`**: A record of how a schema has evolved, including both structural `changes` and `DataTransform` functions to convert data between versions.
132
- * **`SchemaRegistry`**: A system for storing, managing, and retrieving `SchemaDefinition` files and their `Migration` history. This can be in-memory or backed by Git.
133
-
134
- ### Defining Schemas
135
-
136
- Schemas are defined using the `SchemaDefinition` interface. Here's an example:
137
-
138
- ```typescript
139
- import type { SchemaDefinition, FieldType } from '@asaidimu/anansi';
140
-
141
- const addressSchema: SchemaDefinition['nestedSchemas']['address'] = {
142
- name: 'Address', // Name used for referencing this nested schema
143
- description: 'Represents a physical address',
144
- fields: {
145
- street: { name: 'street', type: 'string', required: true, description: 'Street address line' },
146
- city: { name: 'city', type: 'string', required: true },
147
- zipCode: { name: 'zipCode', type: 'string', required: true, hint: { input: { type: 'text', placeholder: 'e.g., 90210' } } }
148
- }
149
- };
150
-
151
- const userSchema: SchemaDefinition = {
152
- name: 'User',
153
- version: '1.0.0',
154
- description: 'Defines a user profile',
155
- fields: {
156
- id: { name: 'id', type: 'string', required: true, unique: true, description: 'Unique user identifier' },
157
- firstName: { name: 'firstName', type: 'string', required: true },
158
- lastName: { name: 'lastName', type: 'string', required: true },
159
- email: {
160
- name: 'email',
161
- type: 'string',
162
- required: true,
163
- unique: true,
164
- constraints: [{ name: 'isEmailFormat', predicate: 'regex', parameters: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, errorMessage: 'Must be a valid email format' }],
165
- hint: { input: { type: 'email' } }
166
- },
167
- status: {
168
- name: 'status',
169
- type: 'enum',
170
- values: ['active', 'inactive', 'suspended'],
171
- default: 'active',
172
- description: 'Current status of the user',
173
- hint: { input: { type: 'select', options: [{value: 'active', label: 'Active'}, {value: 'inactive', label: 'Inactive'}, {value: 'suspended', label: 'Suspended'}] } }
174
- },
175
- address: {
176
- name: 'address',
177
- type: 'object',
178
- schema: { id: 'address' }, // Reference to the nested schema
179
- required: false,
180
- description: 'User\'s residential address'
181
- },
182
- tags: {
183
- name: 'tags',
184
- type: 'set', // Ensures unique items
185
- itemsType: 'string',
186
- default: [],
187
- description: 'A set of keywords associated with the user'
188
- }
189
- },
190
- nestedSchemas: {
191
- address: addressSchema // Define the nested schema here
192
- },
193
- indexes: [
194
- { name: 'emailIndex', fields: ['email'], type: 'unique', description: 'Ensure email uniqueness' },
195
- { name: 'nameCompositeIndex', fields: ['lastName', 'firstName'], type: 'composite' }
196
- ],
197
- constraints: [
198
- {
199
- name: 'fullNameLength',
200
- operator: 'and',
201
- rules: [
202
- { name: 'firstNameMinLength', predicate: 'minLength', field: 'firstName', parameters: 2, errorMessage: 'First name too short' },
203
- { name: 'lastNameMinLength', predicate: 'minLength', field: 'lastName', parameters: 2, errorMessage: 'Last name too short' }
204
- ]
205
- }
206
- ],
207
- // Example of a mock data generator (uses @faker-js/faker)
208
- mock: (faker) => ({
209
- id: faker.string.uuid(),
210
- firstName: faker.person.firstName(),
211
- lastName: faker.person.lastName(),
212
- email: faker.internet.email(),
213
- status: faker.helpers.arrayElement(['active', 'inactive', 'suspended']),
214
- address: {
215
- street: faker.location.streetAddress(),
216
- city: faker.location.city(),
217
- zipCode: faker.location.zipCode()
218
- },
219
- tags: Array.from({length: faker.number.int({min: 1, max: 3})}, () => faker.lorem.word())
220
- })
221
- };
102
+ ```ts
103
+ const client = await AnansiClient.connect("https://api.example.com");
104
+ const { doc } = await client.send("/orders", order);
105
+ console.log(doc.order_id);
222
106
  ```
223
107
 
224
- ### Schema Registry
225
-
226
- The `SchemaRegistry` is responsible for storing and managing your `SchemaDefinition` files and their versions.
227
-
228
- #### Core `SchemaRegistry` (In-Memory)
108
+ **3. Conventions that make servers cooperate.**
229
109
 
230
- ```typescript
231
- import { SchemaRegistry, type SchemaDefinition } from '@asaidimu/anansi';
110
+ | Convention | Why |
111
+ |---|---|
112
+ | `Content-Type: application/vnd.anansi.binary` | The MIME type reserved in the spec (Appendix B); lets servers route binary vs JSON bodies |
113
+ | `X-Anansi-Version` header (or the packet's own embedded version) | Schema pinning — server decodes with exactly the schema you compiled |
114
+ | One packet per HTTP body | Self-delineating framing; no length prefixes or chunking needed |
115
+ | Plain packets from clients | Servers accept them unconditionally; compression/encryption are optional upgrades |
232
116
 
233
- const registry = new SchemaRegistry('/my-app-schemas'); // Uses LightningFS in browser, local files in Node.js
117
+ **4. What the browser gives you for free.**
234
118
 
235
- async function demoRegistry() {
236
- await registry.init(); // Initialize the registry structure
119
+ - All codec paths are pure JS + `Uint8Array` — no Node built-ins.
120
+ - Transforms use WebCrypto (`AES-GCM`), WASM (`BLAKE3`, hash-wasm), and pure
121
+ JS (`fzstd`) — every backend works in a tab or worker.
122
+ - Only *outgoing* zstd compression is unavailable in browsers (no stdlib
123
+ compressor); clients simply send plain packets and still receive compressed
124
+ responses.
237
125
 
238
- const productSchema: SchemaDefinition = {
239
- name: 'Product',
240
- version: '1.0.0',
241
- fields: {
242
- id: { name: 'id', type: 'string', required: true },
243
- name: { name: 'name', type: 'string' },
244
- price: { name: 'price', type: 'number' }
245
- },
246
- nestedSchemas: {}
247
- };
126
+ **5. Validate before you send, after you receive.**
248
127
 
249
- await registry.create({ schema: productSchema });
250
- console.log('Product schema created.');
128
+ ```ts
129
+ await client.validator.validatePartial(patchBody); // PATCH-shaped documents
130
+ ```
251
131
 
252
- const schemas = await registry.list();
253
- console.log('Available schemas:', schemas);
132
+ The same `DocumentValidator` semantics run on both sides of the wire, so
133
+ client-side validation catches what the server would reject — before the
134
+ bytes leave the tab.
254
135
 
255
- const fetchedSchema = await registry.schema({ name: 'Product' });
256
- console.log('Fetched Product schema:', fetchedSchema?.version);
136
+ ## Transforms (compression · integrity · encryption)
257
137
 
258
- // Update schema
259
- const updatedProductSchema = { ...productSchema, version: '1.1.0', description: 'Updated product schema' };
260
- await registry.update({ schema: updatedProductSchema });
261
- console.log('Product schema updated to version:', (await registry.schema({ name: 'Product' }))?.version);
138
+ All async (WebCrypto/WASM), all composable, all browser-safe:
262
139
 
263
- const history = await registry.history({ name: 'Product' });
264
- console.log('Product schema history:', history.map(s => s.version));
140
+ ```ts
141
+ import { encodeAnansiPacket, decodeAnansiPacket } from "@asaidimu/anansi";
265
142
 
266
- // Sync the registry (updates internal lockfile hashes)
267
- await registry.sync();
268
- console.log('Registry synced.');
269
- }
143
+ const sealed = await encodeAnansiPacket(fields, doc, 7, {
144
+ compression: true, // ZSTD (flags bit 2)
145
+ integrity: true, // BLAKE3[0..16) over plaintext (bit 7)
146
+ encryptionKey: key32bytes, // AES-256-GCM (bit 6)
147
+ });
270
148
 
271
- demoRegistry();
149
+ const { doc } = await decodeAnansiPacket(sealed, fields, {
150
+ decryptionKey: key32bytes,
151
+ });
272
152
  ```
273
153
 
274
- #### Git-Enabled `SchemaRegistry`
154
+ Backend matrix:
275
155
 
276
- For persistent storage, collaboration, and advanced versioning, `createGitSchemaRegistry` wraps the core `SchemaRegistry` with `isomorphic-git`. This allows pushing/pulling schemas to a remote Git repository (e.g., GitHub, Gitea).
156
+ | Transform | Browser | Bun / Node |
157
+ |---|---|---|
158
+ | zstd decompress | `fzstd` (pure JS) | `fzstd` |
159
+ | zstd compress | send plain packets¹ | `node:zlib` |
160
+ | BLAKE3-128 | `hash-wasm` | `hash-wasm` |
161
+ | AES-256-GCM | WebCrypto | WebCrypto |
277
162
 
278
- ```typescript
279
- import { createGitSchemaRegistry, SchemaDefinition } from '@asaidimu/anansi';
280
- import { createGithubRepository } from '@asaidimu/anansi/lib/registry/github';
163
+ ¹ Browsers have no stdlib zstd compressor. Servers accept plain packets, so
164
+ this never blocks a client; a WASM compressor can be added later.
281
165
 
282
- // IMPORTANT: Replace with your actual GitHub credentials and desired repository details
283
- const githubRemote = await createGithubRepository({
284
- username: 'your-github-username',
285
- password: 'your-github-personal-access-token', // PAT with repo scope
286
- repository: 'anansi-schemas-repo', // The repo name to use on GitHub
287
- create: true // Will create the repo if it doesn't exist
288
- });
166
+ Order of operations follows the spec: compress encrypt on encode;
167
+ decrypt decompress verify digest over plaintext on decode. Tampered
168
+ packets fail loudly (`integrity check failed`).
289
169
 
290
- async function demoGitRegistry() {
291
- const gitRegistry = await createGitSchemaRegistry(
292
- '/git-registry', // Local directory for the Git clone
293
- {
294
- remote: githubRemote,
295
- mainBranch: 'main',
296
- createRemote: true, // Auto-create remote repo if not exists
297
- author: { name: 'Anansi Bot', email: 'anansi@example.com' },
298
- proxy: 'https://cors.isomorphic-git.org' // Optional CORS proxy for browser environments
299
- }
300
- );
301
-
302
- await gitRegistry.init(); // Initialize local Git repository
303
-
304
- const orderSchema: SchemaDefinition = {
305
- name: 'Order',
306
- version: '1.0.0',
307
- fields: {
308
- orderId: { name: 'orderId', type: 'string', required: true },
309
- amount: { name: 'amount', type: 'number' }
310
- },
311
- nestedSchemas: {}
312
- };
313
-
314
- // Create schema and push to remote
315
- await gitRegistry.create({ schema: orderSchema });
316
- console.log('Order schema created and pushed to Git registry.');
317
-
318
- // Update schema and push to remote
319
- const updatedOrderSchema = { ...orderSchema, version: '1.1.0', fields: { ...orderSchema.fields, status: { name: 'status', type: 'string' } } };
320
- await gitRegistry.update({ schema: updatedOrderSchema });
321
- console.log('Order schema updated and pushed to Git registry.');
322
-
323
- // Pull latest changes from remote (e.g., if another user pushed)
324
- await gitRegistry.sync();
325
- console.log('Git registry synced with remote.');
326
-
327
- // Delete schema and clean up remote tags/branches
328
- await gitRegistry.delete({ name: 'Order' });
329
- console.log('Order schema deleted from Git registry and remote.');
330
- }
170
+ ## Validation
331
171
 
332
- // In a real application, ensure you handle authentication tokens securely
333
- // and avoid hardcoding them.
334
- // demoGitRegistry();
335
- ```
172
+ Documents against schemas, and schemas against the meta-schema:
336
173
 
337
- ### Schema Evolution (Migrations)
338
-
339
- Anansi provides a robust `MigrationEngine` to manage schema changes and transform data.
340
-
341
- ```typescript
342
- import { MigrationEngine, DataTransform, SchemaDefinition } from '@asaidimu/anansi';
343
- import { createSchemaMigrationHelper } from '@asaidimu/anansi';
344
-
345
- // Assume initial schema (e.g., loaded from SchemaRegistry)
346
- let currentSchema: SchemaDefinition = {
347
- name: 'LegacyUser',
348
- version: '1.0.0',
349
- fields: {
350
- legacyId: { name: 'legacyId', type: 'string', required: true },
351
- oldName: { name: 'oldName', type: 'string' }
352
- },
353
- nestedSchemas: {}
354
- };
355
-
356
- const migrationEngine = new MigrationEngine(currentSchema);
357
-
358
- async function performMigration() {
359
- // Define a migration helper for version 1.1.0
360
- const helper = createSchemaMigrationHelper(currentSchema);
361
-
362
- // Schema changes: Rename field, add new field
363
- helper.modifyField('oldName', { name: 'fullName' }); // Renaming field name, not property key
364
- helper.addField('email', { name: 'email', type: 'string', required: true });
365
-
366
- // Data transform: Map oldName to fullName, add a default email
367
- const transform: DataTransform<any, any> = {
368
- forward: (data) => ({
369
- id: data.legacyId, // Assuming 'id' is a new concept mapped from legacyId
370
- fullName: data.oldName,
371
- email: data.oldName.toLowerCase().replace(/\s/g, '.') + '@example.com'
372
- }),
373
- backward: (data) => ({
374
- legacyId: data.id,
375
- oldName: data.fullName,
376
- // Cannot reliably reverse email generation, so might need placeholder
377
- })
378
- };
379
-
380
- const { migrate, rollback } = helper.changes(); // Get both forward and backward changes
381
-
382
- // Add the migration to the engine
383
- await migrationEngine.add({
384
- description: 'Rename oldName to fullName and add email',
385
- changes: migrate,
386
- rollback: rollback,
387
- transform: transform
388
- });
389
-
390
- // Example initial data
391
- let data = [{ legacyId: 'A1', oldName: 'John Doe' }, { legacyId: 'B2', oldName: 'Jane Smith' }];
392
- let dataStream = new ReadableStream({
393
- start(controller) {
394
- data.forEach(item => controller.enqueue(item));
395
- controller.close();
396
- }
397
- });
398
-
399
- console.log('--- Dry Run Migration ---');
400
- const { newSchema: dryRunSchema, dataPreview: dryRunPreview } = await migrationEngine.dryRun(dataStream, 'forward');
401
- const previewData = await new Response(dryRunPreview).json(); // Consume stream
402
- console.log('Simulated New Schema:', dryRunSchema);
403
- console.log('Simulated Data Preview:', previewData);
404
-
405
- // Now, apply the actual migration
406
- console.log('\n--- Applying Migration ---');
407
- const transformedStream = await migrationEngine.migrate(dataStream);
408
- const transformedData = await new Response(transformedStream).json(); // Consume stream
409
- console.log('Transformed Data:', transformedData);
410
- currentSchema = migrationEngine.data().schema; // Update current schema in place
411
- console.log('Actual New Schema Version:', currentSchema.version);
412
-
413
- // Now, rollback the migration (using the updated currentSchema)
414
- console.log('\n--- Rolling Back Migration ---');
415
- const rollbackEngine = new MigrationEngine(currentSchema, migrationEngine.data().migrations, migrationEngine.data().history);
416
- dataStream = new ReadableStream({
417
- start(controller) {
418
- transformedData.forEach(item => controller.enqueue(item));
419
- controller.close();
420
- }
421
- });
422
- const rolledBackStream = await rollbackEngine.rollback(dataStream);
423
- const rolledBackData = await new Response(rolledBackStream).json();
424
- console.log('Rolled Back Data:', rolledBackData);
425
- currentSchema = rollbackEngine.data().schema;
426
- console.log('Rolled Back Schema Version:', currentSchema.version);
427
- }
174
+ ```ts
175
+ import {
176
+ DocumentValidator, SchemaValidator, metaSchemaPredicateMap,
177
+ } from "@asaidimu/anansi";
428
178
 
429
- performMigration();
430
- ```
431
-
432
- ### Runtime Validation
433
-
434
- Anansi generates runtime validators based on your schema definitions.
435
-
436
- ```typescript
437
- import { createStandardSchemaValidator, type SchemaDefinition } from '@asaidimu/anansi';
438
-
439
- const productSchema: SchemaDefinition = {
440
- name: 'Product',
441
- version: '1.0.0',
442
- fields: {
443
- id: { name: 'id', type: 'string', required: true },
444
- name: { name: 'name', type: 'string', required: true },
445
- price: { name: 'price', type: 'number', required: true, constraints: [{ name: 'positivePrice', predicate: 'min', parameters: 0 }] },
446
- category: { name: 'category', type: 'string', required: false, default: 'General' },
447
- inStock: { name: 'inStock', type: 'boolean', required: true }
448
- },
449
- nestedSchemas: {}
450
- };
451
-
452
- // Define custom predicates (validation functions)
453
- const customPredicates = {
454
- min: ({ data, field, arguments: minValue }: { data: any, field: string, arguments: number }) => {
455
- return data[field] >= minValue;
456
- },
457
- // Add other predicates as needed for your constraints
458
- };
459
-
460
- const productValidator = createStandardSchemaValidator(productSchema, customPredicates)['~standard'];
461
-
462
- // Valid data
463
- const validProduct = {
464
- id: 'prod123',
465
- name: 'Laptop Pro',
466
- price: 1200.50,
467
- inStock: true
468
- };
469
-
470
- const validationResult1 = productValidator.validate(validProduct);
471
- console.log('Valid product validation:', validationResult1); // { value: {...} }
472
-
473
- // Invalid data (missing required field)
474
- const invalidProduct1 = {
475
- id: 'prod124',
476
- price: 500,
477
- inStock: false
478
- };
479
-
480
- const validationResult2 = productValidator.validate(invalidProduct1);
481
- console.log('Invalid product (missing name) validation:', validationResult2);
482
- // { issues: [{ message: "Field 'name' is required", path: ["name"] }] }
483
-
484
- // Invalid data (price constraint violation)
485
- const invalidProduct2 = {
486
- id: 'prod125',
487
- name: 'Headphones',
488
- price: -10, // Invalid price
489
- inStock: true
490
- };
491
-
492
- const validationResult3 = productValidator.validate(invalidProduct2);
493
- console.log('Invalid product (price constraint) validation:', validationResult3);
494
- // { issues: [{ message: "Constraint 'positivePrice' failed for field 'price' with params 0", path: ["price", "constraints[0]"] }] }
495
- ```
179
+ const validator = await DocumentValidator.create(schemaJSON, metaSchemaPredicateMap);
180
+ validator.validate(doc); // strict
181
+ validator.validatePartial(patch); // PATCH payloads: skips REQUIRED_FIELD_MISSING
182
+ validator.validateLoose(draft); // also skips UNEXPECTED_FIELD
496
183
 
497
- ### Developer Tools
498
-
499
- Anansi includes utilities to assist developers in building and documenting their applications.
500
-
501
- #### Generating TypeScript Types
502
-
503
- `schemaToTypes` generates TypeScript type definitions from your schema, providing strong typing for your application's data models.
504
-
505
- ```typescript
506
- import { schemaToTypes, type SchemaDefinition } from '@asaidimu/anansi';
507
-
508
- const mySchema: SchemaDefinition = {
509
- name: 'BlogPost',
510
- version: '1.0.0',
511
- description: 'A blog post entry',
512
- fields: {
513
- id: { name: 'id', type: 'string', required: true },
514
- title: { name: 'title', type: 'string', required: true, description: 'The title of the blog post' },
515
- author: { name: 'author', type: 'string', required: false, deprecated: true },
516
- content: { name: 'content', type: 'string' },
517
- status: { name: 'status', type: 'enum', values: ['draft', 'published', 'archived'] },
518
- metadata: { name: 'metadata', type: 'record', required: false, description: 'Arbitrary key-value metadata' },
519
- tags: { name: 'tags', type: 'array', itemsType: 'string', default: [] },
520
- comments: { name: 'comments', type: 'object', schema: { id: 'Comment' }, required: false }
521
- },
522
- nestedSchemas: {
523
- Comment: {
524
- name: 'Comment',
525
- fields: {
526
- commentId: { name: 'commentId', type: 'string', required: true },
527
- text: { name: 'text', type: 'string', required: true },
528
- authorEmail: { name: 'authorEmail', type: 'string', required: true }
529
- }
530
- }
531
- },
532
- indexes: [
533
- { name: 'titleIndex', fields: ['title'], type: 'normal' }
534
- ]
535
- };
536
-
537
- const generatedTypes = schemaToTypes(mySchema, true, true);
538
- console.log(generatedTypes);
539
-
540
- /* Expected Output (simplified):
541
- export type Comment = {
542
- commentId: string;
543
- text: string;
544
- authorEmail: string;
545
- };
546
-
547
- export type BlogPostStatus = "draft" | "published" | "archived";
548
-
549
- export type BlogPost<Metadata extends Record<string, any> = Record<string, any>> = {
550
- id: string;
551
- title: string;
552
- ... rest of fields and types ...
553
- author?: string;
554
- metadata?: Metadata;
555
- tags?: string[];
556
- comments?: string | Comment;
557
- };
558
-
559
- export enum BlogPostIndexNames {
560
- titleIndex = "titleIndex",
561
- }
562
- */
184
+ await SchemaValidator.validate(schemaJSON); // schema ↔ meta-schema conformance
563
185
  ```
564
186
 
565
- #### Generating Markdown Documentation
566
-
567
- `docgen` creates a human-readable Markdown document describing your schema.
568
-
569
- ```typescript
570
- import { docgen, type SchemaDefinition } from '@asaidimu/anansi';
571
- import { faker } from '@faker-js/faker';
572
-
573
- const docSchema: SchemaDefinition = {
574
- name: 'Customer',
575
- version: '1.0.0',
576
- description: 'Detailed profile for a customer.',
577
- fields: {
578
- customerId: { name: 'customerId', type: 'string', required: true, unique: true, description: 'Unique identifier for the customer.' },
579
- fullName: { name: 'fullName', type: 'string', required: true, description: 'Full name of the customer.' },
580
- tier: { name: 'tier', type: 'enum', values: ['Bronze', 'Silver', 'Gold'], default: 'Bronze', description: 'Customer loyalty tier.' },
581
- lastPurchaseDate: { name: 'lastPurchaseDate', type: 'string', required: false, description: 'Date of the last purchase (ISO string).' }
582
- },
583
- nestedSchemas: {},
584
- mock: (fakerInstance) => ({
585
- customerId: fakerInstance.string.uuid(),
586
- fullName: fakerInstance.person.fullName(),
587
- tier: fakerInstance.helpers.arrayElement(['Bronze', 'Silver', 'Gold']),
588
- lastPurchaseDate: fakerInstance.date.past().toISOString()
589
- })
590
- };
591
-
592
- const markdownDoc = docgen(docSchema, { faker });
593
- console.log(markdownDoc);
594
-
595
- /* Expected Output (partial):
596
- # Customer Schema (Version 1.0.0)
597
-
598
- Detailed profile for a customer.
599
-
600
- ## Metadata
601
- - **Dependencies:** None
602
- - **Created:** 2024-XX-XXTXX:XX:XXZ
603
-
604
- ## Fields
605
-
606
- | Name | Type | Required | Default | Description | Deprecated | Unique | Constraints |
607
- |------|------|----------|---------|-------------|------------|--------|-------------|
608
- | customerId | string | Yes | `None` | Unique identifier for the customer. | No | Yes | 0 |
609
- | fullName | string | Yes | `None` | Full name of the customer. | No | No | 0 |
610
- | tier | enum | No | `"Bronze"` | Customer loyalty tier. | No | No | 0 |
611
- | lastPurchaseDate | string | No | `None` | Date of the last purchase (ISO string). | No | No | 0 |
612
-
613
- ## Indexes
614
-
615
- ...
616
- */
187
+ Modes, issue codes, constraint scoping, and predicate semantics mirror the Go
188
+ implementation in `core/schema/definition/validator.go`.
189
+
190
+ ## Semantics worth knowing
191
+
192
+ - **int64 number**: integer fields surface as JS numbers; values beyond
193
+ `Number.MAX_SAFE_INTEGER` throw rather than silently losing precision.
194
+ - **Three field states** (spec §2.7), mapping 1:1 onto JavaScript:
195
+ | JS | Wire state | Dense | Sparse |
196
+ |---|---|---|---|
197
+ | `undefined` / key missing | Not Set | `00`, no bytes | omitted |
198
+ | `null` | Null | `01`, no bytes | DataPoint with null-bit set, no bytes |
199
+ | any other value | Has Value | `10` + encoded value | DataPoint + encoded value |
200
+
201
+ Decoding reverses it: Not Set omits the key, Null restores `null`. Inside
202
+ records/unknown payloads nulls are payload bytes, preserved verbatim.
203
+ (Note: Go's *JSON→document* boundary treats null leaves as absence before
204
+ encoding — that is a JSON-layer choice, not a wire-format one.)
205
+ - **Zero-copy strings** are the default on decode: values view one bulk-copied,
206
+ container-owned backing buffer (one memmove per packet, zero per-string
207
+ allocations). Use `WithCopyStrings` if decoded documents outlive their
208
+ working set.
209
+ - **Bytes** fields are base64 strings in the document model.
210
+ - **Schema versioning**: packets carry a 10-bit `fullVersion`; keep one
211
+ compiled schema per version and decode against it (explicit pinning, no
212
+ silent structural tolerance).
213
+
214
+ ## Conformance
215
+
216
+ This package is generated-and-tested against the Go reference in the same
217
+ repository:
218
+
219
+ - **Linker parity** — TS compile/link reproduces Go's descriptors, DataPoints,
220
+ local offsets, footprints and addresses field-for-field.
221
+ - **Golden vectors** — Go emits real packets (dense/sparse/batch × transform
222
+ combinations); CI replays them byte-for-byte here and re-encodes to identical
223
+ bytes wherever the transform is deterministic.
224
+ - Any drift fails the monorepo's Test workflow before release.
225
+
226
+ ## API surface
227
+
228
+ | Group | Exports |
229
+ |---|---|
230
+ | Schema | `parseSchema`, `Compiler`, `link`, `buildManifest`, types |
231
+ | Packets | `AnansiCodec` (`create`/`encode`/`decode`/`encodeBatch`/…) | recommended facade |
232
+ | `encodeDocument`, `decodeDocument`, `encodeBatchRows`, `encodeBatchColumnar`, `decodeBatch` | functional form |
233
+ | Transforms (async) | `encodeAnansiPacket`, `decodeAnansiPacket`, `encodeAnansiBatchRows`, `encodeAnansiBatchColumnar`, `decodeAnansiBatch` |
234
+ | Validation | `DocumentValidator`, `SchemaValidator`, `metaSchemaPredicateMap` |
235
+
236
+ ## Development
237
+
238
+ ```sh
239
+ bun install
240
+ bun test # unit + golden conformance suites
241
+ bun run build # tsdown → dist (esm/cjs/dts)
242
+ bunx tsc --noEmit # typecheck
617
243
  ```
618
244
 
619
- ## Project Architecture
620
-
621
- Anansi's architecture is modular, promoting separation of concerns and extensibility.
622
-
623
- ### Core Components
624
-
625
- * **`src/types`**: Defines the foundational data structures and interfaces for Anansi. This is the "language" of your data models and how various parts of the system interact.
626
- * **`src/lib`**: Contains the core implementations of Anansi's main functionalities:
627
- * **Persistence**: Manages data operations (CRUD, events, tasks) against a specific backend. `EphemeralCollection` provides an in-memory implementation for quick starts.
628
- * **Registry**: Provides a mechanism for storing and retrieving schema definitions and their histories. `SchemaRegistry` handles local filesystem storage, while `createGitSchemaRegistry` integrates with Git for distributed version control.
629
- * **Migration**: Orchestrates schema evolution, applying changes and transforming data between versions.
630
- * **`src/sdk`**: Houses pluggable Software Development Kit components, such as specific persistence adapters (e.g., PocketBase) and code generators (e.g., static TypeScript validators).
631
- * **`src/tools`**: A collection of cross-cutting utility functions that support various aspects of Anansi, including cryptographic hashing, JSON Patching, type generation, and advanced runtime validation.
632
-
633
- ### Extension Points
634
-
635
- Anansi is designed for extensibility:
636
-
637
- * **Persistence Adapters**: Implement the `Persistence` and `PersistenceCollection` interfaces to connect Anansi to any database or storage solution (e.g., SQL, NoSQL, GraphQL endpoints).
638
- * **Custom Predicates**: Extend the validation system by defining your own `Predicate` functions and including them in the `PredicateMap` when initializing validators.
639
- * **Schema Changes & Transforms**: The `SchemaMigrationHelper` and `DataTransform` types provide a powerful DSL to define custom schema evolutions and data transformations for complex migrations.
640
- * **Remote Repository Adapters**: Implement the `RemoteRepository` interface to integrate the `SchemaRegistry` with other Git hosting services beyond GitHub and Gitea.
641
-
642
- ## Development & Contributing
643
-
644
- We welcome contributions to Anansi! Here's how you can get started:
645
-
646
- ### Local Development Setup
647
-
648
- 1. **Clone the repository:**
649
- ```bash
650
- git clone https://github.com/asaidimu/data-model.git anansi
651
- cd anansi
652
- ```
653
- 2. **Install dependencies:**
654
- ```bash
655
- bun install
656
- ```
657
- (or `npm install` / `yarn install`)
658
- 3. **Build the project:**
659
- ```bash
660
- bun run build
661
- ```
662
-
663
- ### Available Scripts
664
-
665
- * `bun run ci`: Installs dependencies (for CI environments).
666
- * `bun run clean`: Removes the `dist/` directory.
667
- * `bun run prebuild`: Cleans the `dist/` directory and runs `.sync-package.ts` (internal synchronization).
668
- * `bun run build`: Compiles TypeScript source files into `dist/` for CommonJS and ES Modules, generates declaration files (`.d.ts`), and minifies output.
669
- * `bun run build:watch`: Runs the build process in watch mode for continuous compilation during development.
670
- * `bun run postbuild`: Copies `README.md`, `LICENSE.md`, and `dist.package.json` into the `dist/` directory.
671
- * `bun run test`: Runs all unit tests using Vitest.
672
- * `bun run test:ci`: Runs Vitest tests once for CI environments.
673
- * `bun run test:debug`: Runs Vitest in debug mode, useful for debugging tests in an IDE.
674
- * `bun run docs:dev`: Starts the VitePress development server for the documentation.
675
- * `bun run docs:build`: Builds the static VitePress documentation site.
676
- * `bun run ui:dev`: Starts a local Vite development server for the UI (likely a demo or internal tooling).
677
- * `bun run docs:preview`: Previews the built VitePress documentation.
678
-
679
- ### Testing
680
-
681
- Anansi uses [Vitest](https://vitest.dev/) for its test suite.
682
-
683
- * To run all tests:
684
- ```bash
685
- bun run test
686
- ```
687
- * The tests are configured to run in different environments (Node.js and browser via `happy-dom`/`playwright`) to ensure compatibility.
688
- * Test coverage can be generated by running `bun test --coverage`.
689
-
690
- ### Contributing Guidelines
691
-
692
- We welcome contributions! Please follow these guidelines:
693
-
694
- 1. **Fork** the repository and **clone** your fork.
695
- 2. Create a new **branch** for your feature or bug fix: `git checkout -b feature/my-new-feature` or `bugfix/fix-that-bug`.
696
- 3. Ensure your code adheres to existing coding styles (ESLint, Prettier are used internally).
697
- 4. Write **tests** for your changes.
698
- 5. Ensure all tests pass (`bun run test`).
699
- 6. **Commit** your changes with clear, concise messages following [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) (e.g., `feat: add new feature`, `fix: resolve bug`).
700
- 7. **Push** your branch and create a **Pull Request** to the `main` branch.
701
-
702
- ### Issue Reporting
703
-
704
- * **Bug Reports**: If you find a bug, please open an issue on the [GitHub Issues page](https://github.com/asaidimu/data-model/issues). Provide a clear description, steps to reproduce, expected behavior, and your environment details.
705
- * **Feature Requests**: For new features or enhancements, open an issue with a detailed explanation of the proposed functionality and its use case.
706
-
707
- ## Additional Information
708
-
709
- ### Troubleshooting
710
-
711
- * **"Buffer is not defined"**: If you encounter this error in a browser environment, ensure that `window.Buffer = Buffer;` is included in your entry point, as `LightningFS` and `isomorphic-git` might rely on it. Anansi's `registry.ts` already includes this for convenience.
712
- * **CORS Issues with Git Remote**: When using `createGitSchemaRegistry` in a browser, you might hit CORS restrictions. Utilize the `proxy` option with a CORS proxy URL (e.g., `https://cors.isomorphic-git.org`) in the factory function.
713
- * **Migration Checksum Mismatch**: If you modify a migration file after it has been added to the registry, its checksum will no longer match, leading to an error. Always generate new migrations for changes or ensure your migration logic is stable.
714
-
715
- ### Changelog & Roadmap
716
-
717
- Stay up-to-date with the latest changes and future plans:
718
-
719
- * **Changelog**: Refer to the [CHANGELOG.md](CHANGELOG.md) for a detailed history of releases and breaking changes.
720
- * **Roadmap**: Future development plans are typically tracked via GitHub issues and project boards.
721
-
722
- ### License
723
-
724
- Anansi is open-source software licensed under the **MIT License**. You can find the full text in the [LICENSE.md](LICENSE.md) file.
245
+ Golden fixtures come from the Go side:
246
+ `GOLDEN_UPDATE=1 go test ./core/encoding/anansi/ -run TestGenerateGoldenVectors`.
725
247
 
726
- ### Acknowledgments
248
+ Releases are cut by semantic-release on `main` after the Test workflow passes;
249
+ the npm version always matches the Go module tag.
727
250
 
728
- Anansi builds upon the shoulders of giants. We'd like to acknowledge the following projects and libraries that make Anansi possible:
251
+ ## License
729
252
 
730
- * [isomorphic-git](https://isomorphic-git.org/): For bringing Git to JavaScript environments.
731
- * [@isomorphic-git/lightning-fs](https://www.npmjs.com/package/@isomorphic-git/lightning-fs): A super-fast in-memory filesystem.
732
- * [PocketBase](https://pocketbase.io/): For a delightful backend experience.
733
- * [@faker-js/faker](https://fakerjs.dev/): For robust mock data generation.
734
- * [@standard-schema/spec](https://github.com/standard-schema/spec): For a standardized schema validation interface.
735
- * [Bun](https://bun.sh/): For an incredibly fast JavaScript runtime and toolkit.
736
- * [Vitest](https://vitest.dev/): For a blazing-fast unit test framework.
737
- * [VitePress](https://vitepress.dev/): For beautiful and fast documentation.
253
+ AGPL-3.0-or-later. See [`LICENSE.md`](../../LICENSE.md) at the repository root.
738
254
 
739
- ---
255
+ Need different terms? A **commercial (private) license** is available from the
256
+ copyright holder for use cases where the AGPLv3's network-copyleft doesn't fit
257
+ (embedded products, SaaS without source disclosure, etc.). Contact:
258
+ [github.com/asaidimu](https://github.com/asaidimu).