@asaidimu/anansi 3.0.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 +512 -562
- package/index.cjs +29 -29
- package/index.d.cts +9 -14
- package/index.d.ts +9 -14
- package/index.js +29 -29
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,97 +1,70 @@
|
|
|
1
|
-
# Anansi Schema
|
|
1
|
+
# Anansi: A Schema-Driven Data Modeling Toolkit
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
[](LICENSE.md)
|
|
5
|
-
[](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
|
-
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
-
|
|
81
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
114
|
-
const
|
|
115
|
-
|
|
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
|
|
119
|
-
const persistence = createEphemeralPersistence({},
|
|
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
|
-
|
|
119
|
+
verify();
|
|
122
120
|
```
|
|
123
121
|
|
|
124
|
-
|
|
122
|
+
## Usage Documentation
|
|
125
123
|
|
|
126
|
-
|
|
124
|
+
### Core Concepts
|
|
127
125
|
|
|
128
|
-
|
|
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
|
-
|
|
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
|
-
|
|
134
|
+
### Defining Schemas
|
|
149
135
|
|
|
150
|
-
|
|
136
|
+
Schemas are defined using the `SchemaDefinition` interface. Here's an example:
|
|
151
137
|
|
|
152
138
|
```typescript
|
|
153
|
-
import {
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
175
|
-
name:
|
|
176
|
-
version:
|
|
177
|
-
description:
|
|
151
|
+
const userSchema: SchemaDefinition = {
|
|
152
|
+
name: 'User',
|
|
153
|
+
version: '1.0.0',
|
|
154
|
+
description: 'Defines a user profile',
|
|
178
155
|
fields: {
|
|
179
|
-
id: { name:
|
|
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:
|
|
182
|
-
type:
|
|
160
|
+
name: 'email',
|
|
161
|
+
type: 'string',
|
|
183
162
|
required: true,
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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:
|
|
194
|
-
type:
|
|
195
|
-
values: [
|
|
196
|
-
default:
|
|
197
|
-
description:
|
|
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
|
-
|
|
200
|
-
name:
|
|
201
|
-
type:
|
|
202
|
-
schema: { id:
|
|
175
|
+
address: {
|
|
176
|
+
name: 'address',
|
|
177
|
+
type: 'object',
|
|
178
|
+
schema: { id: 'address' }, // Reference to the nested schema
|
|
203
179
|
required: false,
|
|
204
|
-
description:
|
|
180
|
+
description: 'User\'s residential address'
|
|
205
181
|
},
|
|
206
|
-
|
|
207
|
-
name:
|
|
208
|
-
type:
|
|
209
|
-
itemsType:
|
|
210
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
229
|
-
operator:
|
|
199
|
+
name: 'fullNameLength',
|
|
200
|
+
operator: 'and',
|
|
230
201
|
rules: [
|
|
231
|
-
{ name:
|
|
232
|
-
{ name:
|
|
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([
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
219
|
+
tags: Array.from({length: faker.number.int({min: 1, max: 3})}, () => faker.lorem.word())
|
|
246
220
|
})
|
|
247
221
|
};
|
|
248
222
|
```
|
|
249
223
|
|
|
250
|
-
###
|
|
224
|
+
### Schema Registry
|
|
251
225
|
|
|
252
|
-
|
|
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 {
|
|
231
|
+
import { SchemaRegistry, type SchemaDefinition } from '@asaidimu/anansi';
|
|
256
232
|
|
|
257
|
-
const
|
|
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
|
-
|
|
235
|
+
async function demoRegistry() {
|
|
236
|
+
await registry.init(); // Initialize the registry structure
|
|
264
237
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
276
|
-
|
|
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
|
-
|
|
288
|
-
|
|
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
|
-
|
|
294
|
-
|
|
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
|
-
//
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
});
|
|
304
|
-
|
|
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
|
-
//
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
274
|
+
#### Git-Enabled `SchemaRegistry`
|
|
313
275
|
|
|
314
|
-
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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-
|
|
328
|
-
repository: 'anansi-schemas',
|
|
329
|
-
create: true
|
|
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
|
-
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
//
|
|
339
|
-
await
|
|
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
|
-
|
|
342
|
-
await registry.create({ schema: UserSchema });
|
|
343
|
-
console.log(`Schema '${UserSchema.name}' created/updated in registry.`);
|
|
337
|
+
### Schema Evolution (Migrations)
|
|
344
338
|
|
|
345
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
341
|
+
```typescript
|
|
342
|
+
import { MigrationEngine, DataTransform, SchemaDefinition } from '@asaidimu/anansi';
|
|
343
|
+
import { createSchemaMigrationHelper } from '@asaidimu/anansi';
|
|
352
344
|
|
|
353
|
-
//
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
version:
|
|
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
|
-
|
|
359
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
//
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
###
|
|
432
|
+
### Runtime Validation
|
|
375
433
|
|
|
376
|
-
|
|
434
|
+
Anansi generates runtime validators based on your schema definitions.
|
|
377
435
|
|
|
378
436
|
```typescript
|
|
379
|
-
import {
|
|
437
|
+
import { createStandardSchemaValidator, type SchemaDefinition } from '@asaidimu/anansi';
|
|
380
438
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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:
|
|
387
|
-
|
|
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
|
-
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
|
|
425
|
-
|
|
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
|
-
|
|
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
|
-
|
|
431
|
-
console.log('
|
|
432
|
-
|
|
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
|
-
###
|
|
497
|
+
### Developer Tools
|
|
461
498
|
|
|
462
|
-
|
|
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
|
|
468
|
-
name:
|
|
469
|
-
version:
|
|
508
|
+
const mySchema: SchemaDefinition = {
|
|
509
|
+
name: 'BlogPost',
|
|
510
|
+
version: '1.0.0',
|
|
511
|
+
description: 'A blog post entry',
|
|
470
512
|
fields: {
|
|
471
|
-
id: { name:
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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
|
-
|
|
483
|
-
name:
|
|
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
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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(
|
|
537
|
+
const generatedTypes = schemaToTypes(mySchema, true, true);
|
|
505
538
|
console.log(generatedTypes);
|
|
506
539
|
|
|
507
|
-
/*
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
|
547
|
+
export type BlogPostStatus = "draft" | "published" | "archived";
|
|
521
548
|
|
|
522
|
-
export type
|
|
549
|
+
export type BlogPost<Metadata extends Record<string, any> = Record<string, any>> = {
|
|
523
550
|
id: string;
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
|
531
|
-
|
|
559
|
+
export enum BlogPostIndexNames {
|
|
560
|
+
titleIndex = "titleIndex",
|
|
532
561
|
}
|
|
533
|
-
*/
|
|
562
|
+
*/
|
|
534
563
|
```
|
|
535
564
|
|
|
536
|
-
|
|
565
|
+
#### Generating Markdown Documentation
|
|
537
566
|
|
|
538
|
-
|
|
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
|
-
|
|
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
|
-
|
|
547
|
-
|
|
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-
|
|
602
|
+
- **Created:** 2024-XX-XXTXX:XX:XXZ
|
|
556
603
|
|
|
557
604
|
## Fields
|
|
558
605
|
|
|
559
|
-
| Name
|
|
560
|
-
|
|
561
|
-
|
|
|
562
|
-
|
|
|
563
|
-
|
|
|
564
|
-
|
|
|
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
|
|
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
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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
|
-
|
|
644
|
+
We welcome contributions to Anansi! Here's how you can get started:
|
|
683
645
|
|
|
684
|
-
|
|
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
|
|
653
|
+
2. **Install dependencies:**
|
|
692
654
|
```bash
|
|
693
655
|
bun install
|
|
694
656
|
```
|
|
695
|
-
|
|
657
|
+
(or `npm install` / `yarn install`)
|
|
658
|
+
3. **Build the project:**
|
|
696
659
|
```bash
|
|
697
|
-
|
|
660
|
+
bun run build
|
|
698
661
|
```
|
|
699
662
|
|
|
700
663
|
### Available Scripts
|
|
701
664
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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
|
-
|
|
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
|
|
740
|
-
3.
|
|
741
|
-
4. Write
|
|
742
|
-
5. Ensure all tests pass (`bun test`).
|
|
743
|
-
6.
|
|
744
|
-
7. **
|
|
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
|
-
|
|
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
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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
|
-
###
|
|
715
|
+
### Changelog & Roadmap
|
|
760
716
|
|
|
761
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
-
|
|
786
|
-
|
|
787
|
-
|
|
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
|
-
|
|
739
|
+
---
|