@asaidimu/anansi 2.1.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,97 +1,70 @@
1
- # Anansi Schema Evolution Platform
1
+ # Anansi: A Schema-Driven Data Modeling Toolkit
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/@asaidimu/anansi.svg)](https://www.npmjs.com/package/@asaidimu/anansi)
4
- [![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE.md)
5
- [![Build Status](https://img.shields.io/github/actions/workflow/status/asaidimu/data-model/main.yml?branch=main&label=build)](https://github.com/asaidimu/data-model/actions)
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.
6
4
 
7
- A comprehensive toolkit for advanced data modelling, schema evolution, and adaptive persistence management in complex enterprise systems.
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)
8
8
 
9
9
  ---
10
10
 
11
- ## Table of Contents
12
-
13
- - [Why the Name "Anansi"?](#why-the-name-anansi)
14
- - [Theoretical Foundation](#theoretical-foundation)
15
- - [Overview & Features](#overview--features)
16
- - [Detailed Description](#detailed-description)
17
- - [Key Features](#key-features)
18
- - [Installation & Setup](#installation--setup)
19
- - [Prerequisites](#prerequisites)
20
- - [Installation Steps](#installation-steps)
21
- - [Configuration](#configuration)
22
- - [Usage Documentation](#usage-documentation)
23
- - [Defining a Schema](#defining-a-schema)
24
- - [Ephemeral Persistence](#ephemeral-persistence)
25
- - [Schema Registry (Git-backed)](#schema-registry-git-backed)
26
- - [Schema Migration](#schema-migration)
27
- - [Type Generation](#type-generation)
28
- - [Documentation Generation](#documentation-generation)
29
- - [Project Architecture](#project-architecture)
30
- - [Directory Structure](#directory-structure)
31
- - [Core Components](#core-components)
32
- - [Data Flow](#data-flow)
33
- - [Extension Points](#extension-points)
34
- - [Development & Contributing](#development--contributing)
35
- - [Development Setup](#development-setup)
36
- - [Available Scripts](#available-scripts)
37
- - [Testing](#testing)
38
- - [Contributing Guidelines](#contributing-guidelines)
39
- - [Issue Reporting](#issue-reporting)
40
- - [Additional Information](#additional-information)
41
- - [Troubleshooting](#troubleshooting)
42
- - [FAQ](#faq)
43
- - [Changelog](#changelog)
44
- - [License](#license)
45
- - [Acknowledgments](#acknowledgments)
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)
46
36
 
47
37
  ---
48
38
 
49
- ## Why the Name "Anansi"?
50
-
51
- Named after the legendary Akan trickster god of West Africa, our platform embodies Anansi's core attributes:
52
- - **Wisdom in Complexity**: Like Anansi weaving intricate webs, this platform navigates the complex landscapes of enterprise data systems.
53
- - **Adaptive Intelligence**: Anansi's legendary ability to transform and adapt mirrors our platform's approach to schema evolution.
54
- - **Storytelling of Systems**: In Akan folklore, Anansi is a master storyteller who can unravel and reweave narratives—just as our platform manages the intricate narratives of data models.
55
-
56
- The name reflects our philosophical approach: transformative, intelligent, and deeply respectful of the complex systems we seek to understand and evolve.
57
-
58
- ## Theoretical Foundation
59
-
60
- A mathematically rigorous framework for managing data model complexity, grounded in advanced theoretical principles of system evolution and distributed computing.
61
-
62
39
  ## Overview & Features
63
40
 
64
- ### Detailed Description
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.
65
42
 
66
- Anansi is a comprehensive platform built to tackle the challenges of modern enterprise data management. It provides a principled approach to schema design, evolution, and system integration, moving beyond simple CRUD operations to offer a robust, version-controlled environment for your data models. By treating schemas as living, evolving constructs, Anansi enables systematic transformations while preserving system-wide data integrity. Its in-memory capabilities, backed by a persistent Git-enabled registry, ensure both high performance and reliable versioning for collaborative development and deployment. Anansi is ideal for organizations navigating complex, rapidly evolving, and distributed system architectures.
67
-
68
- ### Key Conceptual Components
69
-
70
- - 👷 **Schema Registry**: Inspired by the intricate webs of knowledge, this component offers comprehensive metadata tracking and explicit dependency management for all your data schemas.
71
- - 🧠 **Theoretical Migration Framework**: Provides formal methods for schema evolution, guaranteeing atomic transformations and supporting bidirectional migrations to ensure data consistency across versions.
72
- - 🌐 **Architectural Abstractions**: Enables decoupled schema representation, cross-system consistency models, and adaptive persistence strategies to integrate seamlessly into diverse architectural landscapes.
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.
73
44
 
74
45
  ### Key Features
75
46
 
76
- - **Schema Definition & Validation**: Define complex data structures using a rich `SchemaDefinition` interface, complete with fields, nested schemas, constraints, and indexes. Leverage a powerful, auto-generated validation SDK to ensure data integrity against your defined schemas.
77
- - **Schema Evolution & Migration**: Manage schema changes over time with a robust `MigrationEngine`. Define schema changes and data transformations to seamlessly evolve your data models forward and backward, preserving data consistency.
78
- - **Flexible Persistence Adapters**: Interact with various data stores through a unified `Persistence` interface. Includes an in-memory ephemeral persistence layer for rapid prototyping and a production-ready PocketBase adapter with built-in retry mechanisms and error categorization.
79
- - **Schema Registry with Version Control**: Store, manage, and version your schemas using a `SchemaRegistry` that can operate purely in-memory (backed by `LightningFS`) or integrate with Git for distributed version control, branching, and tagging.
80
- - **Code Generation & Utilities**: Automatically generate TypeScript types (`schemaToTypes`) and human-readable documentation (`docgen`) directly from your `SchemaDefinition` files. Includes essential utilities for cryptographic hashing, deep merging, and JSON Patch operations.
81
- - **Event-driven Data Operations**: Subscribe to granular persistence events (e.g., `create:success`, `update:failed`) and define triggers or scheduled tasks to automate workflows around data changes.
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.
82
54
 
83
55
  ## Installation & Setup
84
56
 
85
57
  ### Prerequisites
86
58
 
87
- - **Node.js**: Version 18.x or higher.
88
- - **Bun**: Recommended for faster installation and script execution. (Alternatively, npm/yarn can be used).
89
- - **TypeScript**: For developing with Anansi and its generated types.
90
- - **PocketBase** (Optional): If using the PocketBase persistence adapter, a running PocketBase instance is required.
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
91
64
 
92
65
  ### Installation Steps
93
66
 
94
- Install Anansi into your project using Bun (recommended) or npm:
67
+ Install Anansi in your project using Bun (recommended) or your preferred package manager:
95
68
 
96
69
  ```bash
97
70
  # Using Bun
@@ -99,691 +72,668 @@ bun add @asaidimu/anansi
99
72
 
100
73
  # Using npm
101
74
  npm install @asaidimu/anansi
75
+
76
+ # Using yarn
77
+ yarn add @asaidimu/anansi
102
78
  ```
103
79
 
104
80
  ### Configuration
105
81
 
106
- #### Ephemeral Persistence (In-memory)
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`.
107
83
 
108
- For local development and testing, you can use the ephemeral persistence layer which requires no external configuration:
84
+ No global configuration files are strictly required, but specific persistence adapters will require their own configuration (e.g., API URLs, authentication tokens).
85
+
86
+ ### Verification
87
+
88
+ You can quickly verify the installation by trying a simple import in a TypeScript file:
109
89
 
110
90
  ```typescript
111
91
  import { createEphemeralPersistence } from '@asaidimu/anansi';
92
+ import type { SchemaDefinition } from '@asaidimu/anansi';
112
93
 
113
- // Define your predicate map (if using custom constraints)
114
- const myPredicates = {
115
- isPositive: ({ data, field, arguments: min }) => data[field] > min,
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
116
103
  };
117
104
 
118
- // Create a new in-memory persistence instance
119
- const persistence = createEphemeralPersistence({}, myPredicates); // No functionMap needed for basic use
105
+ // Create an in-memory persistence instance
106
+ const persistence = createEphemeralPersistence({}, {}); // Requires functionMap and predicateMap (can be empty for basic usage)
107
+
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);
116
+ }
117
+ }
120
118
 
121
- // Now you can create and manage collections in memory
119
+ verify();
122
120
  ```
123
121
 
124
- #### Git-backed Schema Registry
122
+ ## Usage Documentation
125
123
 
126
- To enable persistent, version-controlled schema management, you'll configure `createGitSchemaRegistry` with a remote Git repository (e.g., GitHub, Gitea). This will require authentication credentials.
124
+ ### Core Concepts
127
125
 
128
- ```typescript
129
- import { createGitSchemaRegistry, createGithubRepository } from '@asaidimu/anansi';
130
-
131
- // Example for GitHub
132
- const remoteRepo = await createGithubRepository({
133
- username: process.env.GITHUB_USERNAME!,
134
- password: process.env.GITHUB_PAT!, // Personal Access Token
135
- repository: 'my-anansi-schemas',
136
- create: true, // Auto-create if not exists
137
- });
138
-
139
- const gitRegistry = await createGitSchemaRegistry('/my-schema-registry', {
140
- remote: remoteRepo,
141
- author: { name: 'Anansi Bot', email: 'anansi-bot@example.com' },
142
- // proxy: 'https://cors.isomorphic-git.org', // Uncomment if encountering CORS issues
143
- });
126
+ Anansi revolves around several core concepts:
144
127
 
145
- await gitRegistry.init(); // Initialize the local Git repository and sync with remote
146
- ```
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.
147
133
 
148
- #### PocketBase Persistence
134
+ ### Defining Schemas
149
135
 
150
- To use PocketBase, you need to provide the PocketBase URL and optionally an auth token:
136
+ Schemas are defined using the `SchemaDefinition` interface. Here's an example:
151
137
 
152
138
  ```typescript
153
- import { createPocketBasePersistence } from '@asaidimu/anansi/sdk/pocketbase'; // Note: path to specific SDK
154
- import { createSchemaMigrationHelper } from '@asaidimu/anansi/lib/schema/helpers'; // Note: direct import for helper
155
-
156
- const pocketBasePersistence = createPocketBasePersistence({
157
- url: 'http://127.0.0.1:8090', // Your PocketBase instance URL
158
- // authToken: 'YOUR_POCKETBASE_AUTH_TOKEN', // Optional: if auth is required
159
- env: 'development', // 'development' for migrations/rollbacks, 'production' for read-only
160
- });
161
-
162
- // Use pocketBasePersistence like any other Anansi persistence instance
163
- ```
139
+ import type { SchemaDefinition, FieldType } from '@asaidimu/anansi';
164
140
 
165
- ## Usage Documentation
166
-
167
- ### Defining a Schema
168
-
169
- Schemas are defined using the `SchemaDefinition` interface. Here's a basic example:
170
-
171
- ```typescript
172
- import { SchemaDefinition } from '@asaidimu/anansi';
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
+ };
173
150
 
174
- const UserSchema: SchemaDefinition = {
175
- name: "User",
176
- version: "1.0.0",
177
- description: "Represents a user in the system",
151
+ const userSchema: SchemaDefinition = {
152
+ name: 'User',
153
+ version: '1.0.0',
154
+ description: 'Defines a user profile',
178
155
  fields: {
179
- id: { name: "id", type: "string", required: true, description: "Unique user ID" },
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 },
180
159
  email: {
181
- name: "email",
182
- type: "string",
160
+ name: 'email',
161
+ type: 'string',
183
162
  required: true,
184
- constraints: [{
185
- name: "isEmail",
186
- predicate: "isEmailFormat", // Assumes 'isEmailFormat' is in your predicateMap
187
- parameters: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
188
- errorMessage: "Must be a valid email address."
189
- }],
190
- description: "User's email address"
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' } }
191
166
  },
192
167
  status: {
193
- name: "status",
194
- type: "enum",
195
- values: ["active", "inactive", "pending"],
196
- default: "pending",
197
- description: "User's current 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'}] } }
198
174
  },
199
- profile: {
200
- name: "profile",
201
- type: "object",
202
- schema: { id: "UserProfile" }, // References a nested schema
175
+ address: {
176
+ name: 'address',
177
+ type: 'object',
178
+ schema: { id: 'address' }, // Reference to the nested schema
203
179
  required: false,
204
- description: "User's profile details"
180
+ description: 'User\'s residential address'
205
181
  },
206
- roles: {
207
- name: "roles",
208
- type: "set",
209
- itemsType: "string",
210
- description: "Set of unique roles assigned to the user"
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'
211
188
  }
212
189
  },
213
190
  nestedSchemas: {
214
- UserProfile: {
215
- name: "UserProfile",
216
- fields: {
217
- firstName: { name: "firstName", type: "string", required: true },
218
- lastName: { name: "lastName", type: "string", required: true },
219
- age: { name: "age", type: "number", required: false, default: 18 }
220
- }
221
- }
191
+ address: addressSchema // Define the nested schema here
222
192
  },
223
193
  indexes: [
224
- { name: "emailIndex", fields: ["email"], type: "unique", description: "Ensures unique email addresses" }
194
+ { name: 'emailIndex', fields: ['email'], type: 'unique', description: 'Ensure email uniqueness' },
195
+ { name: 'nameCompositeIndex', fields: ['lastName', 'firstName'], type: 'composite' }
225
196
  ],
226
197
  constraints: [
227
198
  {
228
- name: "ageConstraint",
229
- operator: "and",
199
+ name: 'fullNameLength',
200
+ operator: 'and',
230
201
  rules: [
231
- { name: "minAge", predicate: "min", field: "profile.age", parameters: 18 },
232
- { name: "maxAge", predicate: "max", field: "profile.age", parameters: 120 }
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' }
233
204
  ]
234
205
  }
235
206
  ],
207
+ // Example of a mock data generator (uses @faker-js/faker)
236
208
  mock: (faker) => ({
237
209
  id: faker.string.uuid(),
210
+ firstName: faker.person.firstName(),
211
+ lastName: faker.person.lastName(),
238
212
  email: faker.internet.email(),
239
- status: faker.helpers.arrayElement(["active", "inactive", "pending"]),
240
- profile: {
241
- firstName: faker.person.firstName(),
242
- lastName: faker.person.lastName(),
243
- age: faker.number.int({ min: 18, max: 99 })
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()
244
218
  },
245
- roles: faker.helpers.arrayElements(["admin", "user", "guest"], { min: 1, max: 3 })
219
+ tags: Array.from({length: faker.number.int({min: 1, max: 3})}, () => faker.lorem.word())
246
220
  })
247
221
  };
248
222
  ```
249
223
 
250
- ### Ephemeral Persistence
224
+ ### Schema Registry
251
225
 
252
- Interact with data using the in-memory persistence layer:
226
+ The `SchemaRegistry` is responsible for storing and managing your `SchemaDefinition` files and their versions.
227
+
228
+ #### Core `SchemaRegistry` (In-Memory)
253
229
 
254
230
  ```typescript
255
- import { createEphemeralPersistence, SchemaDefinition } from '@asaidimu/anansi';
231
+ import { SchemaRegistry, type SchemaDefinition } from '@asaidimu/anansi';
256
232
 
257
- const myPredicates = {
258
- isEmailFormat: ({ data, field, arguments: regex }) => regex.test(data[field]),
259
- min: ({ data, field, arguments: minValue }) => data[field] >= minValue,
260
- max: ({ data, field, arguments: maxValue }) => data[field] <= maxValue,
261
- };
233
+ const registry = new SchemaRegistry('/my-app-schemas'); // Uses LightningFS in browser, local files in Node.js
262
234
 
263
- const persistence = createEphemeralPersistence({}, myPredicates);
235
+ async function demoRegistry() {
236
+ await registry.init(); // Initialize the registry structure
264
237
 
265
- // Assume UserSchema is defined as above
266
- const usersCollection = await persistence.createCollection<typeof UserSchema>({
267
- name: UserSchema.name,
268
- version: UserSchema.version,
269
- fields: UserSchema.fields,
270
- nestedSchemas: UserSchema.nestedSchemas,
271
- constraints: UserSchema.constraints,
272
- indexes: UserSchema.indexes,
273
- });
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
+ };
274
248
 
275
- // 🚀 Create
276
- const newUser = await usersCollection.create({
277
- data: {
278
- id: 'user123',
279
- email: 'test@example.com',
280
- status: 'active',
281
- profile: { firstName: 'John', lastName: 'Doe', age: 30 },
282
- roles: ['admin', 'user'],
283
- },
284
- });
285
- console.log('Created user:', newUser);
249
+ await registry.create({ schema: productSchema });
250
+ console.log('Product schema created.');
286
251
 
287
- // Read
288
- const activeUsers = await usersCollection.read({
289
- query: { filters: { status: { $eq: 'active' } } },
290
- });
291
- console.log('Active users:', activeUsers);
252
+ const schemas = await registry.list();
253
+ console.log('Available schemas:', schemas);
292
254
 
293
- // 🔄 Update
294
- const updatedUsers = await usersCollection.update({
295
- query: { email: { $eq: 'test@example.com' } },
296
- data: { status: 'inactive' },
297
- });
298
- console.log('Updated users:', updatedUsers);
255
+ const fetchedSchema = await registry.schema({ name: 'Product' });
256
+ console.log('Fetched Product schema:', fetchedSchema?.version);
299
257
 
300
- // 🗑️ Delete
301
- const deletedCount = await usersCollection.delete({
302
- query: { id: { $eq: 'user123' } },
303
- });
304
- console.log('Deleted users count:', deletedCount);
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);
262
+
263
+ const history = await registry.history({ name: 'Product' });
264
+ console.log('Product schema history:', history.map(s => s.version));
305
265
 
306
- // Validate
307
- const invalidUser = { id: 'invalid', email: 'bad-email' };
308
- const validationResult = usersCollection.validate(invalidUser);
309
- console.log('Validation issues:', validationResult.issues);
266
+ // Sync the registry (updates internal lockfile hashes)
267
+ await registry.sync();
268
+ console.log('Registry synced.');
269
+ }
270
+
271
+ demoRegistry();
310
272
  ```
311
273
 
312
- ### Schema Registry (Git-backed)
274
+ #### Git-Enabled `SchemaRegistry`
313
275
 
314
- Manage your schemas persistently with Git integration.
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).
315
277
 
316
278
  ```typescript
317
- import {
318
- createGitSchemaRegistry,
319
- createGithubRepository,
320
- SchemaDefinition,
321
- } from '@asaidimu/anansi';
322
- import { UserSchema } from './my-schemas'; // Assuming UserSchema is defined
323
-
324
- // Configure remote repository (e.g., GitHub)
325
- const githubRepo = await createGithubRepository({
279
+ import { createGitSchemaRegistry, SchemaDefinition } from '@asaidimu/anansi';
280
+ import { createGithubRepository } from '@asaidimu/anansi/lib/registry/github';
281
+
282
+ // IMPORTANT: Replace with your actual GitHub credentials and desired repository details
283
+ const githubRemote = await createGithubRepository({
326
284
  username: 'your-github-username',
327
- password: 'your-github-pat', // Use a Personal Access Token
328
- repository: 'anansi-schemas',
329
- create: true, // Create the repo if it doesn't exist
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
330
288
  });
331
289
 
332
- // Create Git-backed schema registry
333
- const registry = await createGitSchemaRegistry('/anansi-schemas', {
334
- remote: githubRepo,
335
- author: { name: 'Anansi Bot', email: 'bot@anansi.com' },
336
- });
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.');
337
326
 
338
- // Initialize the registry (clones/creates local repo, syncs)
339
- await registry.init();
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
+ }
331
+
332
+ // In a real application, ensure you handle authentication tokens securely
333
+ // and avoid hardcoding them.
334
+ // demoGitRegistry();
335
+ ```
340
336
 
341
- // 👷 Create a schema
342
- await registry.create({ schema: UserSchema });
343
- console.log(`Schema '${UserSchema.name}' created/updated in registry.`);
337
+ ### Schema Evolution (Migrations)
344
338
 
345
- // 📋 List all schemas
346
- const allSchemas = await registry.list();
347
- console.log('All schemas:', allSchemas);
339
+ Anansi provides a robust `MigrationEngine` to manage schema changes and transform data.
348
340
 
349
- // 🔍 Retrieve a schema definition
350
- const retrievedSchema = await registry.schema({ name: 'User' });
351
- console.log('Retrieved User schema:', retrievedSchema?.version);
341
+ ```typescript
342
+ import { MigrationEngine, DataTransform, SchemaDefinition } from '@asaidimu/anansi';
343
+ import { createSchemaMigrationHelper } from '@asaidimu/anansi';
352
344
 
353
- // 🔄 Update a schema
354
- const updatedUserSchema: SchemaDefinition = {
355
- ...UserSchema,
356
- version: "1.1.0",
345
+ // Assume initial schema (e.g., loaded from SchemaRegistry)
346
+ let currentSchema: SchemaDefinition = {
347
+ name: 'LegacyUser',
348
+ version: '1.0.0',
357
349
  fields: {
358
- ...UserSchema.fields,
359
- phone: { name: "phone", type: "string", required: false, description: "User's phone number" }
360
- }
350
+ legacyId: { name: 'legacyId', type: 'string', required: true },
351
+ oldName: { name: 'oldName', type: 'string' }
352
+ },
353
+ nestedSchemas: {}
361
354
  };
362
- await registry.update({ schema: updatedUserSchema });
363
- console.log(`Schema '${updatedUserSchema.name}' updated to v${updatedUserSchema.version}.`);
364
355
 
365
- // Sync local changes with remote Git repository
366
- await registry.sync();
367
- console.log('Registry synced with remote Git repository.');
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
+ });
368
389
 
369
- // 🗑️ Delete a schema
370
- // await registry.delete({ name: 'User' });
371
- // console.log(`Schema 'User' deleted.`);
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
+ }
428
+
429
+ performMigration();
372
430
  ```
373
431
 
374
- ### Schema Migration
432
+ ### Runtime Validation
375
433
 
376
- Apply and roll back schema changes, including data transformations.
434
+ Anansi generates runtime validators based on your schema definitions.
377
435
 
378
436
  ```typescript
379
- import { createEphemeralPersistence, createSchemaMigrationHelper, SchemaDefinition, DataTransform } from '@asaidimu/anansi';
437
+ import { createStandardSchemaValidator, type SchemaDefinition } from '@asaidimu/anansi';
380
438
 
381
- // Assume UserSchema is defined as above
382
- const oldUserSchema: SchemaDefinition = {
383
- ...UserSchema,
384
- version: "1.0.0", // Original version
439
+ const productSchema: SchemaDefinition = {
440
+ name: 'Product',
441
+ version: '1.0.0',
385
442
  fields: {
386
- id: { name: "id", type: "string", required: true },
387
- oldEmail: { name: "oldEmail", type: "string", required: false } // Field to be migrated
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 }
388
448
  },
389
449
  nestedSchemas: {}
390
450
  };
391
451
 
392
- const persistence = createEphemeralPersistence({}, {});
393
- const usersCollection = await persistence.createCollection<any>({
394
- name: oldUserSchema.name,
395
- version: oldUserSchema.version,
396
- fields: oldUserSchema.fields,
397
- nestedSchemas: oldUserSchema.nestedSchemas
398
- });
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
+ };
399
459
 
400
- // Add some dummy data with oldEmail
401
- await usersCollection.create({
402
- data: [
403
- { id: 'u1', oldEmail: 'user1@old.com', status: 'active' },
404
- { id: 'u2', oldEmail: 'user2@old.com', status: 'inactive' }
405
- ]
406
- });
460
+ const productValidator = createStandardSchemaValidator(productSchema, customPredicates)['~standard'];
407
461
 
408
- // Define the migration
409
- const addEmailMigration = (h: ReturnType<typeof createSchemaMigrationHelper>) => {
410
- // Define forward transformation: oldEmail -> email
411
- const forwardTransform: DataTransform<any, any>['forward'] = (data) => ({
412
- ...data,
413
- email: data.oldEmail,
414
- oldEmail: undefined // Remove old field
415
- });
462
+ // Valid data
463
+ const validProduct = {
464
+ id: 'prod123',
465
+ name: 'Laptop Pro',
466
+ price: 1200.50,
467
+ inStock: true
468
+ };
416
469
 
417
- // Define backward transformation: email -> oldEmail
418
- const backwardTransform: DataTransform<any, any>['backward'] = (data) => ({
419
- ...data,
420
- oldEmail: data.email,
421
- email: undefined // Remove new field
422
- });
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
+ };
423
479
 
424
- h.addField('email', { name: 'email', type: 'string', required: true, description: 'New email field' });
425
- h.removeField('oldEmail'); // Deprecate/remove old field
480
+ const validationResult2 = productValidator.validate(invalidProduct1);
481
+ console.log('Invalid product (missing name) validation:', validationResult2);
482
+ // { issues: [{ message: "Field 'name' is required", path: ["name"] }] }
426
483
 
427
- return { forward: forwardTransform, backward: backwardTransform };
484
+ // Invalid data (price constraint violation)
485
+ const invalidProduct2 = {
486
+ id: 'prod125',
487
+ name: 'Headphones',
488
+ price: -10, // Invalid price
489
+ inStock: true
428
490
  };
429
491
 
430
- // ➡️ Perform a dry run migration
431
- console.log('\n--- Dry Run Migration (Forward) ---');
432
- const { newSchema: dryRunSchema, dataPreview } = await usersCollection.migrate(
433
- 'Migrate oldEmail to email',
434
- addEmailMigration,
435
- true // dryRun = true
436
- );
437
- console.log('Dry run new schema version:', dryRunSchema.version);
438
- const previewRecords = await new Response(dataPreview).json(); // Read from stream
439
- console.log('Dry run data preview:', previewRecords);
440
-
441
- // 🚀 Apply the actual migration
442
- console.log('\n--- Applying Migration (Forward) ---');
443
- await usersCollection.migrate(
444
- 'Migrate oldEmail to email',
445
- addEmailMigration,
446
- false // dryRun = false
447
- );
448
- console.log('Migration applied. Current schema version:', usersCollection.schema().version);
449
- const migratedData = await usersCollection.read({});
450
- console.log('Migrated data:', migratedData);
451
-
452
- // ⏪ Rollback the migration (if supported by persistence)
453
- console.log('\n--- Rolling Back Migration ---');
454
- await usersCollection.rollback(undefined, false); // Rollback to previous version
455
- console.log('Rolled back. Current schema version:', usersCollection.schema().version);
456
- const rolledBackData = await usersCollection.read({});
457
- console.log('Rolled back data:', rolledBackData);
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]"] }] }
458
495
  ```
459
496
 
460
- ### Type Generation
497
+ ### Developer Tools
461
498
 
462
- Generate TypeScript types from your schema definitions.
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.
463
504
 
464
505
  ```typescript
465
- import { schemaToTypes, SchemaDefinition } from '@asaidimu/anansi';
506
+ import { schemaToTypes, type SchemaDefinition } from '@asaidimu/anansi';
466
507
 
467
- const ProductSchema: SchemaDefinition = {
468
- name: "Product",
469
- version: "1.0.0",
508
+ const mySchema: SchemaDefinition = {
509
+ name: 'BlogPost',
510
+ version: '1.0.0',
511
+ description: 'A blog post entry',
470
512
  fields: {
471
- id: { name: "id", type: "string", required: true },
472
- name: { name: "name", type: "string", required: true },
473
- price: { name: "price", type: "number", required: true },
474
- currency: { name: "currency", type: "enum", values: ["USD", "EUR", "GBP"] },
475
- details: {
476
- name: "details",
477
- type: "object",
478
- schema: { id: "ProductDetails" }
479
- }
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 }
480
521
  },
481
522
  nestedSchemas: {
482
- ProductDetails: {
483
- name: "ProductDetails",
484
- fields: {
485
- weight: { name: "weight", type: "number" },
486
- dimensions: {
487
- name: "dimensions",
488
- type: "object",
489
- schema: { id: "ProductDimensions" }
490
- }
491
- }
492
- },
493
- ProductDimensions: {
494
- name: "ProductDimensions",
523
+ Comment: {
524
+ name: 'Comment',
495
525
  fields: {
496
- length: { name: "length", type: "number" },
497
- width: { name: "width", type: "number" },
498
- height: { name: "height", type: "number" }
526
+ commentId: { name: 'commentId', type: 'string', required: true },
527
+ text: { name: 'text', type: 'string', required: true },
528
+ authorEmail: { name: 'authorEmail', type: 'string', required: true }
499
529
  }
500
530
  }
501
- }
531
+ },
532
+ indexes: [
533
+ { name: 'titleIndex', fields: ['title'], type: 'normal' }
534
+ ]
502
535
  };
503
536
 
504
- const generatedTypes = schemaToTypes(ProductSchema);
537
+ const generatedTypes = schemaToTypes(mySchema, true, true);
505
538
  console.log(generatedTypes);
506
539
 
507
- /*
508
- // Output will be similar to:
509
- export type ProductDimensions = {
510
- length?: number;
511
- width?: number;
512
- height?: number;
513
- };
514
-
515
- export type ProductDetails = {
516
- weight?: number;
517
- dimensions?: ProductDimensions;
540
+ /* Expected Output (simplified):
541
+ export type Comment = {
542
+ commentId: string;
543
+ text: string;
544
+ authorEmail: string;
518
545
  };
519
546
 
520
- export type ProductCurrency = "USD" | "EUR" | "GBP";
547
+ export type BlogPostStatus = "draft" | "published" | "archived";
521
548
 
522
- export type Product = {
549
+ export type BlogPost<Metadata extends Record<string, any> = Record<string, any>> = {
523
550
  id: string;
524
- name: string;
525
- price: number;
526
- currency: ProductCurrency;
527
- details?: string | ProductDetails; // "string" for concrete schemas reference
551
+ title: string;
552
+ ... rest of fields and types ...
553
+ author?: string;
554
+ metadata?: Metadata;
555
+ tags?: string[];
556
+ comments?: string | Comment;
528
557
  };
529
558
 
530
- export enum ProductIndexNames {
531
- ...
559
+ export enum BlogPostIndexNames {
560
+ titleIndex = "titleIndex",
532
561
  }
533
- */
562
+ */
534
563
  ```
535
564
 
536
- ### Documentation Generation
565
+ #### Generating Markdown Documentation
537
566
 
538
- Generate markdown documentation for your schemas.
567
+ `docgen` creates a human-readable Markdown document describing your schema.
539
568
 
540
569
  ```typescript
541
- import { docgen, SchemaDefinition } from '@asaidimu/anansi';
570
+ import { docgen, type SchemaDefinition } from '@asaidimu/anansi';
542
571
  import { faker } from '@faker-js/faker';
543
572
 
544
- // Assume ProductSchema is defined as above
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);
545
594
 
546
- const productDoc = docgen(ProductSchema, { faker });
547
- console.log(productDoc);
595
+ /* Expected Output (partial):
596
+ # Customer Schema (Version 1.0.0)
548
597
 
549
- /*
550
- // Output will be similar to:
551
- # Product Schema (Version 1.0.0)
598
+ Detailed profile for a customer.
552
599
 
553
600
  ## Metadata
554
601
  - **Dependencies:** None
555
- - **Created:** 2024-01-01T00:00:00.000Z
602
+ - **Created:** 2024-XX-XXTXX:XX:XXZ
556
603
 
557
604
  ## Fields
558
605
 
559
- | Name | Type | Required | Default | Description | Deprecated | Unique | Constraints |
560
- |----------|--------|----------|---------|--------------------|------------|--------|-------------|
561
- | id | string | Yes | `None` | | No | No | 0 |
562
- | name | string | Yes | `None` | | No | No | 0 |
563
- | price | number | Yes | `None` | | No | No | 0 |
564
- | currency | enum | Yes | `"USD"` | | No | No | 0 |
565
- | details | object | No | `None` | | No | No | 0 |
566
-
567
- ### Nested Schema: ProductDetails
568
-
569
- #### weight (number)
570
-
571
- **Required:** No
572
-
573
- #### dimensions (object)
574
-
575
- ##### length (number)
576
-
577
- **Required:** No
578
-
579
- ##### width (number)
580
-
581
- **Required:** No
582
-
583
- ##### height (number)
584
-
585
- **Required:** No
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 |
586
612
 
587
613
  ## Indexes
588
614
 
589
- | Name | Type | Fields | Unique | Order | Partial Condition | Description |
590
- |------|--------|---------|--------|-------|-------------------|-------------|
591
- | ... | ... | ... | ... | ... | ... | ... |
592
-
593
- ## Constraints
594
- ### Schema-level Constraints
595
615
  ...
596
-
597
- ## Migrations
598
-
599
- | ID | Description | Status | Changes |
600
- |----|-------------|--------|---------|
601
- | ...| ... | ... | ... |
602
-
603
- ## Example Data
604
- ```json
605
- {
606
- "id": "e221b3a4-c5d6-7890-a1b2-c3d4e5f67890",
607
- "name": "Ergonomic Widget",
608
- "price": 99.99,
609
- "currency": "USD",
610
- "details": {
611
- "weight": 0.5,
612
- "dimensions": {
613
- "length": 10,
614
- "width": 5,
615
- "height": 2
616
- }
617
- }
618
- }
619
616
  */
620
617
  ```
621
618
 
622
619
  ## Project Architecture
623
620
 
624
- Anansi is structured to provide a modular and extensible platform for data model management.
625
-
626
- ### Directory Structure
627
-
628
- ```
629
- .
630
- ├── src/
631
- │ ├── lib/ # Core libraries for persistence, registry, migration, schema
632
- │ │ ├── persistence/ # In-memory and adapter-based data persistence (EmphemeralCollection)
633
- │ │ ├── registry/ # Schema Registry (LightningFS + Git integration)
634
- │ │ ├── migration/ # Schema migration engine (MigrationEngine)
635
- │ │ └── schema/ # Schema validation, helpers, and utilities
636
- │ ├── sdk/ # Specific SDK implementations (e.g., PocketBase adapter, static validators)
637
- │ ├── types/ # Core TypeScript interfaces (SchemaDefinition, Persistence, Migration, etc.)
638
- │ └── tools/ # General utilities (crypto, merge, patch, typegen, docgen, validator, version)
639
- ├── docs/ # VitePress documentation site
640
- ├── tests/ # Unit and integration tests
641
- ├── public/ # Public assets for UI (e.g., Vite SVG)
642
- ├── index.ts # Main entry point for the Anansi library
643
- ├── package.json # Project metadata and dependencies
644
- ├── dist.package.json # Package.json for distributed npm package
645
- ├── vitest.config.ts # Vitest configuration for testing
646
- ├── vite.config.ts # Vite configuration for UI development
647
- └── tsconfig.json # TypeScript configuration
648
- ```
621
+ Anansi's architecture is modular, promoting separation of concerns and extensibility.
649
622
 
650
623
  ### Core Components
651
624
 
652
- - **`SchemaDefinition` (`src/types/schema-definition.ts`)**: The central contract defining the structure of data models, including fields, nested schemas, constraints, and migrations.
653
- - **`Persistence` (`src/types/persistence.ts`)**: An abstract interface for all data storage operations (create, read, update, delete, subscribe), designed for pluggable backends.
654
- - **`EmphemeralCollection` (`src/lib/persistence/collection.ts`)**: An in-memory implementation of `PersistenceCollection` for rapid development.
655
- - **PocketBase Adapter (`src/sdk/pocketbase/index.tsx`)**: A concrete `Persistence` implementation for PocketBase, handling schema-driven collection management, migrations, and eventing.
656
- - **`MigrationEngine` (`src/lib/migration/index.ts`)**: Manages the evolution of schemas over time, applying schema changes and data transformations in a controlled, versioned manner.
657
- - **`SchemaRegistry` (`src/lib/registry/registry.ts`)**: The core component for storing and versioning schema definitions. It uses `LightningFS` for in-memory storage.
658
- - **`createGitSchemaRegistry` (`src/lib/registry/git-registry.ts`)**: A factory function that wraps `SchemaRegistry` with `isomorphic-git` to provide persistent, distributed version control for schemas.
659
- - **`Validators` (`src/tools/validator.ts`, `src/lib/schema/validators.ts`)**: Provides utilities for validating data against `SchemaDefinition` rules and for validating the schema definitions themselves.
660
- - **`Type Generators` (`src/tools/typegen.ts`)**: Automatically generates TypeScript type definitions from `SchemaDefinition` files, ensuring strict type safety across your codebase.
661
- - **`Documentation Generator` (`src/tools/docgen.ts`)**: Generates human-readable Markdown documentation for schemas, including fields, indexes, constraints, and example mock data.
662
-
663
- ### Data Flow
664
-
665
- 1. **Schema Definition**: Developers define data models using the `SchemaDefinition` interface.
666
- 2. **Schema Registration**: Schemas are added to the `SchemaRegistry` (either in-memory or Git-backed) for version control and discovery.
667
- 3. **Persistence Layer**: A `Persistence` instance (e.g., `EphemeralPersistence`, `PocketBasePersistence`) is initialized with optional predicates and functions for query and validation.
668
- 4. **Collection Interaction**: Developers interact with collections (e.g., `usersCollection.create()`, `usersCollection.read()`) through the `PersistenceCollection` interface.
669
- 5. **Validation**: All data operations trigger internal validation against the collection's `SchemaDefinition` using the `createStandardSchemaValidator`.
670
- 6. **Schema Evolution**: When data models change, `SchemaChanges` are defined, and the `MigrationEngine` applies these changes to both the schema definition and existing data.
671
- 7. **Synchronization (Git)**: For Git-backed registries, changes are committed and pushed, enabling collaborative schema evolution and traceability.
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.
672
632
 
673
633
  ### Extension Points
674
634
 
675
- - **Custom Persistence Adapters**: Implement the `Persistence` and `PersistenceCollection` interfaces to integrate Anansi with any new data storage backend.
676
- - **Custom Predicates**: Define custom validation logic (e.g., `isEmailFormat`, `isStrongPassword`) and provide them to `createEphemeralPersistence` or any other persistence implementation.
677
- - **Data Transforms**: Write custom `forward` and `backward` transformation functions for migrations, enabling complex data shape changes between schema versions.
678
- - **Schema Hints**: Extend `InputHint` and `SchemaHint` to guide UI generation or other tooling based on schema metadata.
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.
679
641
 
680
642
  ## Development & Contributing
681
643
 
682
- ### Development Setup
644
+ We welcome contributions to Anansi! Here's how you can get started:
683
645
 
684
- To set up the project for local development:
646
+ ### Local Development Setup
685
647
 
686
648
  1. **Clone the repository:**
687
649
  ```bash
688
650
  git clone https://github.com/asaidimu/data-model.git anansi
689
651
  cd anansi
690
652
  ```
691
- 2. **Install dependencies using Bun (recommended):**
653
+ 2. **Install dependencies:**
692
654
  ```bash
693
655
  bun install
694
656
  ```
695
- If you don't have Bun, you can use npm:
657
+ (or `npm install` / `yarn install`)
658
+ 3. **Build the project:**
696
659
  ```bash
697
- npm install
660
+ bun run build
698
661
  ```
699
662
 
700
663
  ### Available Scripts
701
664
 
702
- The `package.json` includes several scripts for development workflows:
703
-
704
- - `bun ci`: Installs dependencies.
705
- - `bun clean`: Removes the `dist` directory.
706
- - `bun prebuild`: Cleans and runs `./.sync-package.ts`.
707
- - `bun build`: Compiles TypeScript files to `dist/` for CJS and ESM formats, generates declaration files, and minifies.
708
- - `bun build:watch`: Runs `build` in watch mode for continuous compilation.
709
- - `bun postbuild`: Copies `README.md`, `LICENSE.md`, and `dist.package.json` into the `dist` directory.
710
- - `bun test`: Runs unit and integration tests using Vitest.
711
- - `bun test:ci`: Runs Vitest tests in CI mode (runs once, exits).
712
- - `bun test:debug`: Runs Vitest with debugger attached.
713
- - `bun docs:dev`: Starts the VitePress development server for documentation.
714
- - `bun docs:build`: Builds the static VitePress documentation site.
715
- - `bun ui:dev`: Starts the Vite development server for the example UI.
716
- - `bun docs:preview`: Previews the built documentation site.
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.
717
678
 
718
679
  ### Testing
719
680
 
720
681
  Anansi uses [Vitest](https://vitest.dev/) for its test suite.
721
682
 
722
- To run all tests:
723
- ```bash
724
- bun test
725
- ```
726
-
727
- To run tests in CI mode (non-interactive):
728
- ```bash
729
- bun test:ci
730
- ```
731
-
732
- Tests include coverage checks and are configured to run in a `happy-dom` environment with `fake-indexeddb` for browser-like filesystem operations (`LightningFS`).
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`.
733
689
 
734
690
  ### Contributing Guidelines
735
691
 
736
692
  We welcome contributions! Please follow these guidelines:
737
693
 
738
694
  1. **Fork** the repository and **clone** your fork.
739
- 2. Create a **new branch** for your feature or bug fix: `git checkout -b feature/my-new-feature` or `bugfix/fix-some-bug`.
740
- 3. Make your changes, ensuring code adheres to existing style and conventions.
741
- 4. Write or update **tests** for your changes to ensure proper functionality and maintain test coverage.
742
- 5. Ensure all tests pass (`bun test`).
743
- 6. Use **semantic commit messages** (e.g., `feat: add new feature`, `fix: resolve bug`). This project uses `semantic-release`.
744
- 7. **Open a Pull Request** to the `main` branch of the upstream repository.
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.
745
701
 
746
702
  ### Issue Reporting
747
703
 
748
- For bugs, feature requests, or questions, please open an issue on our [GitHub Issues page](https://github.com/asaidimu/data-model/issues).
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.
749
706
 
750
707
  ## Additional Information
751
708
 
752
709
  ### Troubleshooting
753
710
 
754
- - **`Buffer is not defined` error**: If running in a browser environment, ensure that `window.Buffer = Buffer;` is included as done in `src/lib/registry/registry.ts`.
755
- - **CORS issues with `isomorphic-git` or PocketBase**: If interacting with remote Git repositories or PocketBase from a browser, you might need a CORS proxy. Configure `createGitSchemaRegistry` or `createPocketBasePersistence` with a `proxy` URL.
756
- - **Migration `TRANSFORM_ERROR`**: Ensure your `DataTransform` functions are correctly defined and handle all expected input shapes. For remote transforms, verify the URL and module export.
757
- - **Git `fastForwardOnly` errors**: When performing `git pull` or `git push`, if `fastForwardOnly` is enabled and conflicts exist, the operation might fail. Consider resolving conflicts manually or disable `fastForwardOnly` if acceptable for your workflow.
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.
758
714
 
759
- ### FAQ
715
+ ### Changelog & Roadmap
760
716
 
761
- - **What is Anansi primarily designed for?**
762
- Anansi is designed for managing complex enterprise data models, focusing on schema evolution, data integrity, and flexible persistence across distributed systems. It's a comprehensive toolkit, not just a simple ORM or validation library.
763
- - **How does Anansi handle data transformations during migrations?**
764
- Anansi uses `DataTransform` objects within migrations, which contain explicit `forward` and `backward` functions. These functions are executed on data streams to transform data shapes as the schema evolves.
765
- - **Is Anansi production-ready?**
766
- Yes, Anansi is built with production use cases in mind, emphasizing theoretical rigor, data integrity, and extensible architecture. The PocketBase adapter and Git-backed schema registry provide production-grade capabilities for persistence and version control.
767
- - **Can I use Anansi with other databases?**
768
- Yes, Anansi is designed with a pluggable `Persistence` interface. You can create custom adapters for any database or data source by implementing this interface.
717
+ Stay up-to-date with the latest changes and future plans:
769
718
 
770
- ### Changelog
771
-
772
- For a detailed history of changes, features, and bug fixes, please refer to the [CHANGELOG.md](CHANGELOG.md) file.
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.
773
721
 
774
722
  ### License
775
723
 
776
- This project is licensed under the MIT License. See the [LICENSE.md](LICENSE.md) file for details.
724
+ Anansi is open-source software licensed under the **MIT License**. You can find the full text in the [LICENSE.md](LICENSE.md) file.
777
725
 
778
726
  ### Acknowledgments
779
727
 
780
- Anansi draws inspiration from and builds upon several foundational technologies and concepts:
728
+ Anansi builds upon the shoulders of giants. We'd like to acknowledge the following projects and libraries that make Anansi possible:
781
729
 
782
- - **SQL Pragmatism**: For the approach to schema definition, constraints, and indexing.
783
- - **`isomorphic-git`**: For enabling Git operations in diverse JavaScript environments.
784
- - **`LightningFS`**: For providing a performant in-memory filesystem abstraction.
785
- - **PocketBase**: For its powerful real-time backend capabilities, integrated via a dedicated persistence adapter.
786
- - **`@faker-js/faker`**: For robust mock data generation capabilities.
787
- - **`@standard-schema/spec`**: For providing a standardized schema validation specification.
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.
788
738
 
789
- We are grateful to the creators and maintainers of these projects for their invaluable contributions to the open-source ecosystem.
739
+ ---