@asaidimu/anansi 4.0.2 → 8.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +204 -685
- package/index.cjs +1 -65
- package/index.d.cts +926 -2192
- package/index.d.mts +1081 -0
- package/index.mjs +1 -0
- package/package.json +31 -37
- package/LICENSE.md +0 -21
- package/index.d.ts +0 -2347
- package/index.js +0 -65
package/README.md
CHANGED
|
@@ -1,739 +1,258 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @asaidimu/anansi
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Self-contained TypeScript implementation of the **Anansi binary wire format** —
|
|
4
|
+
the schema-driven, high-performance serialization used by the Go-Anansi
|
|
5
|
+
persistence layer. Compile schemas, address fields, encode/decode packets, and
|
|
6
|
+
validate documents — all in TypeScript, byte-compatible with the Go
|
|
7
|
+
implementation.
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-

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