@docstack/client 0.0.1
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 +214 -0
- package/lib/core/attribute.d.ts +174 -0
- package/lib/core/attribute.js +296 -0
- package/lib/core/attribute.js.map +1 -0
- package/lib/core/class.d.ts +340 -0
- package/lib/core/class.js +540 -0
- package/lib/core/class.js.map +1 -0
- package/lib/core/crypto-engine/index.d.ts +121 -0
- package/lib/core/crypto-engine/index.js +130 -0
- package/lib/core/crypto-engine/index.js.map +1 -0
- package/lib/core/crypto-engine/utils.d.ts +20 -0
- package/lib/core/crypto-engine/utils.js +76 -0
- package/lib/core/crypto-engine/utils.js.map +1 -0
- package/lib/core/datamodel/index.d.ts +3 -0
- package/lib/core/datamodel/index.js +1186 -0
- package/lib/core/datamodel/index.js.map +1 -0
- package/lib/core/domain.d.ts +200 -0
- package/lib/core/domain.js +285 -0
- package/lib/core/domain.js.map +1 -0
- package/lib/core/index.d.ts +176 -0
- package/lib/core/index.js +379 -0
- package/lib/core/index.js.map +1 -0
- package/lib/core/job-engine/index.d.ts +110 -0
- package/lib/core/job-engine/index.js +116 -0
- package/lib/core/job-engine/index.js.map +1 -0
- package/lib/core/policy-engine/index.d.ts +97 -0
- package/lib/core/policy-engine/index.js +150 -0
- package/lib/core/policy-engine/index.js.map +1 -0
- package/lib/core/query-engine/accumulators.d.ts +9 -0
- package/lib/core/query-engine/accumulators.js +258 -0
- package/lib/core/query-engine/accumulators.js.map +1 -0
- package/lib/core/query-engine/evaluator.d.ts +37 -0
- package/lib/core/query-engine/evaluator.js +179 -0
- package/lib/core/query-engine/evaluator.js.map +1 -0
- package/lib/core/query-engine/executor.d.ts +14 -0
- package/lib/core/query-engine/executor.js +405 -0
- package/lib/core/query-engine/executor.js.map +1 -0
- package/lib/core/query-engine/index.d.ts +4 -0
- package/lib/core/query-engine/index.js +4 -0
- package/lib/core/query-engine/index.js.map +1 -0
- package/lib/core/query-engine/parser.d.ts +10 -0
- package/lib/core/query-engine/parser.js +515 -0
- package/lib/core/query-engine/parser.js.map +1 -0
- package/lib/core/query-engine/planner.d.ts +27 -0
- package/lib/core/query-engine/planner.js +330 -0
- package/lib/core/query-engine/planner.js.map +1 -0
- package/lib/core/stack.d.ts +497 -0
- package/lib/core/stack.js +1507 -0
- package/lib/core/stack.js.map +1 -0
- package/lib/core/test-utils/docstack.d.ts +29 -0
- package/lib/core/test-utils/docstack.js +222 -0
- package/lib/core/test-utils/docstack.js.map +1 -0
- package/lib/core/trigger/index.d.ts +31 -0
- package/lib/core/trigger/index.js +81 -0
- package/lib/core/trigger/index.js.map +1 -0
- package/lib/index.d.ts +4 -0
- package/lib/index.js +8237 -0
- package/lib/index.js.map +1 -0
- package/lib/plugins/pouchdb.d.ts +9 -0
- package/lib/plugins/pouchdb.js +403 -0
- package/lib/plugins/pouchdb.js.map +1 -0
- package/lib/utils/crypto/index.d.ts +3 -0
- package/lib/utils/crypto/index.js +34 -0
- package/lib/utils/crypto/index.js.map +1 -0
- package/lib/utils/index.d.ts +4 -0
- package/lib/utils/index.js +58 -0
- package/lib/utils/index.js.map +1 -0
- package/lib/utils/logger/index.d.ts +4 -0
- package/lib/utils/logger/index.js +20 -0
- package/lib/utils/logger/index.js.map +1 -0
- package/lib/utils/logger/transport.d.ts +11 -0
- package/lib/utils/logger/transport.js +28 -0
- package/lib/utils/logger/transport.js.map +1 -0
- package/lib/workers/dataModel.d.ts +1 -0
- package/lib/workers/dataModel.js +48 -0
- package/lib/workers/dataModel.js.map +1 -0
- package/package.json +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
[](https://www.paypal.com/donate/?hosted_button_id=4QSQ8L9AK2C74)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
# @docstack/client
|
|
5
|
+
|
|
6
|
+
**The Intelligent, Offline-First Database Engine for Modern Web Apps.**
|
|
7
|
+
|
|
8
|
+
DocStack Client is a standalone, browser-based datastore built on PouchDB. It bridges the gap between simple client-side storage and full-fledged backend databases by bringing schemas, relationships, SQL-like querying, and background processing directly to the browser.
|
|
9
|
+
|
|
10
|
+
It is designed for applications that require **offline capabilities**, **strong data consistency**, and **secure local storage**, all while sharing the same powerful data model as the DocStack server.
|
|
11
|
+
|
|
12
|
+
## 🚀 Why DocStack?
|
|
13
|
+
|
|
14
|
+
### Business Advantages
|
|
15
|
+
|
|
16
|
+
- **Logic as Data**: Store business rules, validation scripts, and background jobs as documents in the database. Update your application's behavior dynamically without redeploying the entire codebase.
|
|
17
|
+
- **Zero-Latency UX**: By running the database locally, your UI updates instantly. Data syncs to the server in the background when a connection is available.
|
|
18
|
+
- **Enterprise-Grade Security**: Built-in field-level encryption ensures that sensitive user data (PII, health records) is encrypted *before* it hits the disk, protecting it even if the device is compromised.
|
|
19
|
+
|
|
20
|
+
### Technical Highlights
|
|
21
|
+
|
|
22
|
+
- **Structured NoSQL**: Enjoy the flexibility of JSON documents with the rigor of strict schemas (powered by Zod).
|
|
23
|
+
- **SQL in the Browser**: Stop writing complex map/reduce functions. Query your local data using standard SQL syntax, including `JOIN`s, `UNION`s, and aggregations.
|
|
24
|
+
- **Reactive Architecture**: Subscribe to changes on specific classes or documents to update your UI in real-time.
|
|
25
|
+
- **Background Jobs**: Offload heavy processing (data cleanup, report generation) to background workers that run independently of the UI thread.
|
|
26
|
+
|
|
27
|
+
## 📦 Installation
|
|
28
|
+
|
|
29
|
+
To add the client to your project, use your preferred package manager.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install @docstack/client
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## ⚡ Quick Start
|
|
36
|
+
|
|
37
|
+
Initialize the stack and create your first data model.
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { ClientStack, Class, Attribute } from '@docstack/client';
|
|
41
|
+
|
|
42
|
+
// 1. Initialize the stack (creates a local PouchDB instance)
|
|
43
|
+
const stack = await ClientStack.create('my-app-db');
|
|
44
|
+
|
|
45
|
+
// 2. Define a 'Task' class
|
|
46
|
+
const taskClass = await Class.create(stack, 'Task', 'class', 'User Tasks');
|
|
47
|
+
|
|
48
|
+
// 3. Add attributes to the schema
|
|
49
|
+
await Attribute.create(taskClass, 'title', 'string', 'Task Title', { mandatory: true });
|
|
50
|
+
await Attribute.create(taskClass, 'isComplete', 'boolean', 'Done?', { defaultValue: false });
|
|
51
|
+
|
|
52
|
+
// 4. Create a document
|
|
53
|
+
const myTask = await taskClass.add({
|
|
54
|
+
title: 'Install DocStack',
|
|
55
|
+
isComplete: true
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
console.log('Created Task:', myTask);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 📚 Core Features & Usage
|
|
62
|
+
|
|
63
|
+
### 1. Advanced Querying (SQL-like)
|
|
64
|
+
|
|
65
|
+
DocStack includes a powerful query engine that translates SQL into optimized PouchDB selectors and in-memory operations.
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
// Find all high-priority tasks, join with Assignee, and sort
|
|
69
|
+
const { rows } = await stack.query(`
|
|
70
|
+
SELECT t.title, u.username AS assignee
|
|
71
|
+
FROM Task AS t
|
|
72
|
+
JOIN User AS u ON u._id = t.assigneeId
|
|
73
|
+
WHERE t.priority = 'high' AND t.isComplete = false
|
|
74
|
+
ORDER BY t.createdAt DESC
|
|
75
|
+
`);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 2. Relationships & Domains
|
|
79
|
+
|
|
80
|
+
In addition to foreign keys, define strict relationships between data types using Domains.
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import { Domain } from '@docstack/client';
|
|
84
|
+
|
|
85
|
+
// Define a 1:N relationship between Projects and Tasks
|
|
86
|
+
const domain = await Domain.create(
|
|
87
|
+
stack,
|
|
88
|
+
null,
|
|
89
|
+
'ProjectTasks',
|
|
90
|
+
'domain',
|
|
91
|
+
'1:N',
|
|
92
|
+
projectClass,
|
|
93
|
+
taskClass
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// Now you can traverse relationships easily or enforce referential integrity.
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 3. Background Jobs
|
|
100
|
+
|
|
101
|
+
Define executable logic stored in the database. Jobs run in a sandboxed environment and can be triggered manually or by system events.
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
// Define a job that archives old tasks
|
|
105
|
+
const archiveJob = {
|
|
106
|
+
_id: 'Job-ArchiveOldTasks',
|
|
107
|
+
'~class': '~Job',
|
|
108
|
+
name: 'Archive Tasks',
|
|
109
|
+
type: 'user',
|
|
110
|
+
workerPlatform: 'client',
|
|
111
|
+
isEnabled: true,
|
|
112
|
+
content: `
|
|
113
|
+
async function execute(stack, params) {
|
|
114
|
+
const { rows } = await stack.query("SELECT _id FROM Task WHERE isComplete = true");
|
|
115
|
+
for (const row of rows) {
|
|
116
|
+
// Custom logic to move task to archive...
|
|
117
|
+
console.log('Archiving:', row._id);
|
|
118
|
+
}
|
|
119
|
+
return { metadata: { archivedCount: rows.length } };
|
|
120
|
+
}
|
|
121
|
+
`
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Save the job definition
|
|
125
|
+
await stack.db.bulkDocs([archiveJob]);
|
|
126
|
+
|
|
127
|
+
// Execute it
|
|
128
|
+
const run = await stack.jobEngine.executeJob('Job-ArchiveOldTasks');
|
|
129
|
+
console.log('Job Status:', run.status);
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### 4. Triggers
|
|
133
|
+
|
|
134
|
+
Define custom data validations and transformations that execute automatically before or after document operations. Trigger logic is stored as data and hydrated at runtime.
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
// This trigger automatically generates a URL-friendly slug from a document's title
|
|
138
|
+
const generateSlugTrigger = {
|
|
139
|
+
name: 'generate-slug-from-title',
|
|
140
|
+
order: 'before',
|
|
141
|
+
run: `document.slug = document.title.toLowerCase().replace(/\\s+/g, '-'); return document;`
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Add the trigger to a class schema
|
|
145
|
+
await blogPostClass.addTrigger(generateSlugTrigger);
|
|
146
|
+
|
|
147
|
+
// Now when you save a BlogPost, the slug is generated automatically
|
|
148
|
+
const post = await blogPostClass.add({ title: 'Hello World' });
|
|
149
|
+
console.log(post.slug); // 'hello-world'
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### 5. Access Policies
|
|
153
|
+
|
|
154
|
+
Control who can read or write data with granular, rule-based policies. Great for implementing role-based access control (RBAC) and multi-tenant applications.
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
// Policy: Only users in the 'editors' group can write to Article documents
|
|
158
|
+
const editorsOnlyWritePolicy = {
|
|
159
|
+
_id: 'Policy-Article-EditorsWrite',
|
|
160
|
+
'~class': '~Policy',
|
|
161
|
+
targetClass: ['Class-Article'],
|
|
162
|
+
groupId: 'Group-Editors',
|
|
163
|
+
rule: `
|
|
164
|
+
// 'session' and 'document' are injected at runtime
|
|
165
|
+
return session && session.sessionStatus === 'active';
|
|
166
|
+
`
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// Policy: Published articles are readable by anyone
|
|
170
|
+
const publicReadPolicy = {
|
|
171
|
+
_id: 'Policy-Article-PublicRead',
|
|
172
|
+
'~class': '~Policy',
|
|
173
|
+
targetClass: ['Class-Article'],
|
|
174
|
+
rule: `
|
|
175
|
+
if (document.status === 'published') {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
`
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// Save policies to activate them
|
|
182
|
+
await stack.db.bulkDocs([editorsOnlyWritePolicy, publicReadPolicy]);
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### 6. Security & Encryption
|
|
186
|
+
|
|
187
|
+
Protect sensitive data transparently.
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
// Define a class with an encrypted field
|
|
191
|
+
await Attribute.create(userClass, 'socialSecurityNumber', 'string', 'SSN', {
|
|
192
|
+
encrypted: true
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// When you save a document, 'socialSecurityNumber' is encrypted using a document secure key.
|
|
196
|
+
// It is stored as ciphertext in PouchDB and only decrypted when accessed via the API.
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## 🧩 Architecture
|
|
200
|
+
|
|
201
|
+
DocStack Client is composed of several modular engines:
|
|
202
|
+
|
|
203
|
+
| Engine | Description |
|
|
204
|
+
|--------|-------------|
|
|
205
|
+
| **Core DB** | PouchDB for storage and sync |
|
|
206
|
+
| **Schema Engine** | Zod-based validation and schema hydration |
|
|
207
|
+
| **Query Engine** | SQL parser and planner for complex data retrieval |
|
|
208
|
+
| **Job Engine** | Manages asynchronous tasks and background workers |
|
|
209
|
+
| **Crypto Engine** | Handles key derivation (PBKDF2) and AES-GCM encryption |
|
|
210
|
+
| **Policy Engine** | Enforces granular read/write access rules per class or user-targeted, based on user sessions and document's content |
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
Built with ❤️ for the modern web.
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import z, { ZodType } from "zod";
|
|
2
|
+
import Class from "./class.js";
|
|
3
|
+
import { Attribute as Attribute_, AttributeModel, AttributeType, AttributeTypeConfig } from "@docstack/shared";
|
|
4
|
+
/**
|
|
5
|
+
* Represents a single attribute (field) within a Class schema.
|
|
6
|
+
*
|
|
7
|
+
* Attributes define the structure of documents, including their type,
|
|
8
|
+
* validation rules (via Zod), and configuration options like mandatory,
|
|
9
|
+
* default values, primary keys, and foreign key references.
|
|
10
|
+
*
|
|
11
|
+
* Use the static factory method {@link Attribute.create} to instantiate
|
|
12
|
+
* attributes with proper validation and persistence.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* // Create a string attribute
|
|
17
|
+
* const titleAttr = await Attribute.create(taskClass, 'title', 'string', 'Task Title', {
|
|
18
|
+
* mandatory: true,
|
|
19
|
+
* maxLength: 200
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* // Create a foreign key reference
|
|
23
|
+
* const assigneeAttr = await Attribute.create(taskClass, 'assigneeId', 'foreign_key', 'Assigned User', {
|
|
24
|
+
* targetClass: 'User'
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @extends Attribute_
|
|
29
|
+
*/
|
|
30
|
+
declare class Attribute extends Attribute_ {
|
|
31
|
+
/** The attribute name used as the document field key. */
|
|
32
|
+
name: string;
|
|
33
|
+
/** Optional description explaining the attribute's purpose. */
|
|
34
|
+
description?: string;
|
|
35
|
+
/** The underlying AttributeModel containing type and configuration. */
|
|
36
|
+
model: AttributeModel;
|
|
37
|
+
/** Zod schema for runtime validation of this field. */
|
|
38
|
+
field: ZodType;
|
|
39
|
+
/** Reference to the parent Class that owns this attribute. */
|
|
40
|
+
class: Class | null;
|
|
41
|
+
/** Default value to use when the field is not provided. */
|
|
42
|
+
defaultValue?: any;
|
|
43
|
+
/**
|
|
44
|
+
* Creates a new Attribute instance.
|
|
45
|
+
* For most use cases, prefer using {@link Attribute.create} which also persists the attribute.
|
|
46
|
+
*
|
|
47
|
+
* @param classObj - The parent Class for this attribute
|
|
48
|
+
* @param name - The attribute name
|
|
49
|
+
* @param type - The attribute type (e.g., 'string', 'integer', 'boolean', 'enum')
|
|
50
|
+
* @param description - Optional description
|
|
51
|
+
* @param config - Type-specific configuration options
|
|
52
|
+
*/
|
|
53
|
+
constructor(classObj: Class | null, name: string, type: AttributeType["type"], description?: string, config?: AttributeType["config"]);
|
|
54
|
+
/**
|
|
55
|
+
* Validates that reference-type attributes have proper domain configuration.
|
|
56
|
+
* Called automatically during {@link Attribute.create}.
|
|
57
|
+
* @throws Error if the reference configuration is invalid
|
|
58
|
+
*/
|
|
59
|
+
ensureReferenceConfigIsValid: () => Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Builds the Zod validation schema based on attribute type and configuration.
|
|
62
|
+
* Called automatically during construction.
|
|
63
|
+
*/
|
|
64
|
+
setField: () => void;
|
|
65
|
+
/**
|
|
66
|
+
* Creates a new Attribute and persists it to the parent Class.
|
|
67
|
+
* This is the primary factory method for creating attributes.
|
|
68
|
+
*
|
|
69
|
+
* @param classObj - The parent Class to add the attribute to
|
|
70
|
+
* @param name - The attribute name
|
|
71
|
+
* @param type - The attribute type
|
|
72
|
+
* @param description - Optional description
|
|
73
|
+
* @param config - Type-specific configuration
|
|
74
|
+
* @returns The created Attribute instance
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```typescript
|
|
78
|
+
* const priceAttr = await Attribute.create(productClass, 'price', 'decimal', 'Product price', {
|
|
79
|
+
* min: 0,
|
|
80
|
+
* precision: 2,
|
|
81
|
+
* mandatory: true
|
|
82
|
+
* });
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
static create(classObj: Class, name: string, type: AttributeType["type"], description?: string, config?: AttributeType["config"]): Promise<Attribute>;
|
|
86
|
+
/**
|
|
87
|
+
* Checks if this attribute is marked as a primary key.
|
|
88
|
+
* @returns `true` if this is a primary key attribute
|
|
89
|
+
*/
|
|
90
|
+
isPrimaryKey: () => boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Checks if this attribute is mandatory (required).
|
|
93
|
+
* @returns `true` if the attribute is mandatory
|
|
94
|
+
*/
|
|
95
|
+
isMandatory: () => boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Returns the AttributeModel for this attribute.
|
|
98
|
+
* @returns The underlying AttributeModel
|
|
99
|
+
*/
|
|
100
|
+
getModel: () => AttributeModel;
|
|
101
|
+
/**
|
|
102
|
+
* Returns the parent Class for this attribute.
|
|
103
|
+
* @returns The Class instance
|
|
104
|
+
* @throws Error if the attribute has no parent class
|
|
105
|
+
*/
|
|
106
|
+
getClass: () => Class;
|
|
107
|
+
/**
|
|
108
|
+
* Validates a value against this attribute's Zod schema.
|
|
109
|
+
*
|
|
110
|
+
* @param data - The value to validate
|
|
111
|
+
* @returns Zod safe parse result with success status and data/error
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```typescript
|
|
115
|
+
* const result = await priceAttr.validate(19.99);
|
|
116
|
+
* if (result.success) {
|
|
117
|
+
* console.log('Valid:', result.data);
|
|
118
|
+
* } else {
|
|
119
|
+
* console.log('Invalid:', result.error);
|
|
120
|
+
* }
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
validate: (data: any) => Promise<z.ZodSafeParseResult<unknown>>;
|
|
124
|
+
/**
|
|
125
|
+
* Adds an attribute to its parent class and persists to the database.
|
|
126
|
+
* Used internally by {@link Attribute.create}.
|
|
127
|
+
*
|
|
128
|
+
* @param attributeObj - The Attribute instance to build
|
|
129
|
+
* @returns The Attribute instance
|
|
130
|
+
* @throws Error if the class has no stack connection
|
|
131
|
+
*/
|
|
132
|
+
static build: (attributeObj: Attribute) => Promise<Attribute>;
|
|
133
|
+
setModel: (model: AttributeModel) => void;
|
|
134
|
+
getType: (type: AttributeType["type"]) => "string" | "boolean" | "object" | "integer" | "date" | "decimal" | "foreign_key" | "enum" | "reference";
|
|
135
|
+
/**
|
|
136
|
+
* Returns an empty/default value for this attribute.
|
|
137
|
+
* Uses the Zod schema's default value if configured.
|
|
138
|
+
* @returns Object with attribute name as key and default/null value
|
|
139
|
+
*/
|
|
140
|
+
getEmpty: () => {
|
|
141
|
+
[x: string]: unknown;
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Returns the attribute name.
|
|
145
|
+
* @returns The attribute name string
|
|
146
|
+
*/
|
|
147
|
+
getName: () => string;
|
|
148
|
+
checkTypeValidity: (type: string) => boolean;
|
|
149
|
+
getTypeConf: (type: AttributeType["type"], config: AttributeType["config"] | undefined) => AttributeTypeConfig | ({
|
|
150
|
+
maxLength?: number;
|
|
151
|
+
defaultValue?: string;
|
|
152
|
+
} & AttributeTypeConfig) | ({
|
|
153
|
+
max?: number;
|
|
154
|
+
min?: number;
|
|
155
|
+
defaultValue?: number;
|
|
156
|
+
} & AttributeTypeConfig) | ({
|
|
157
|
+
format?: string;
|
|
158
|
+
defaultValue?: string | number;
|
|
159
|
+
max?: string | number;
|
|
160
|
+
min?: string | number;
|
|
161
|
+
} & AttributeTypeConfig) | ({
|
|
162
|
+
max?: number;
|
|
163
|
+
min?: number;
|
|
164
|
+
precision?: number;
|
|
165
|
+
defaultValue?: number;
|
|
166
|
+
} & AttributeTypeConfig) | ({
|
|
167
|
+
defaultValue?: boolean;
|
|
168
|
+
} & AttributeTypeConfig) | ({
|
|
169
|
+
targetClass?: string;
|
|
170
|
+
} & AttributeTypeConfig) | ({
|
|
171
|
+
domain: string;
|
|
172
|
+
} & AttributeTypeConfig);
|
|
173
|
+
}
|
|
174
|
+
export default Attribute;
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
var _a;
|
|
2
|
+
import z from "zod";
|
|
3
|
+
import { Attribute as Attribute_, ATTRIBUTE_TYPES } from "@docstack/shared";
|
|
4
|
+
class Attribute extends Attribute_ {
|
|
5
|
+
constructor(classObj = null, name, type, description, config) {
|
|
6
|
+
super(classObj, name, type, config);
|
|
7
|
+
this.field = z.any();
|
|
8
|
+
this.ensureReferenceConfigIsValid = async () => {
|
|
9
|
+
if (this.model.type !== "reference") {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if (!this.class) {
|
|
13
|
+
throw new Error(`Attribute '${this.name}' must belong to a class to validate reference configuration.`);
|
|
14
|
+
}
|
|
15
|
+
const stack = this.class.getStack();
|
|
16
|
+
if (!stack) {
|
|
17
|
+
throw new Error(`Class '${this.class.getName()}' is not attached to a stack.`);
|
|
18
|
+
}
|
|
19
|
+
const config = this.model.config;
|
|
20
|
+
const domainName = config.domain;
|
|
21
|
+
if (typeof domainName !== "string" || domainName.length === 0) {
|
|
22
|
+
throw new Error(`Attribute '${this.name}' of type 'reference' must declare a domain.`);
|
|
23
|
+
}
|
|
24
|
+
if (config.isArray) {
|
|
25
|
+
throw new Error(`Attribute '${this.name}' of type 'reference' cannot be an array.`);
|
|
26
|
+
}
|
|
27
|
+
const domain = await stack.getDomain(domainName);
|
|
28
|
+
if (!domain) {
|
|
29
|
+
throw new Error(`Domain '${domainName}' was not found for attribute '${this.name}'.`);
|
|
30
|
+
}
|
|
31
|
+
const classId = this.class.id;
|
|
32
|
+
switch (domain.relation) {
|
|
33
|
+
case "1:N":
|
|
34
|
+
if (classId !== domain.targetClass.id) {
|
|
35
|
+
throw new Error(`Given classId '${classId}' Reference attributes for domain '${domainName}' can only be added to class '${domain.targetClass}'.`);
|
|
36
|
+
}
|
|
37
|
+
break;
|
|
38
|
+
case "N:1":
|
|
39
|
+
if (classId !== domain.sourceClass.id) {
|
|
40
|
+
throw new Error(`Given classId '${classId}' Reference attributes for domain '${domainName}' can only be added to class '${domain.sourceClass}'.`);
|
|
41
|
+
}
|
|
42
|
+
break;
|
|
43
|
+
case "1:1":
|
|
44
|
+
if (classId !== domain.sourceClass.id && classId !== domain.targetClass.id) {
|
|
45
|
+
throw new Error(`Class '${classId}' is not part of domain '${domainName}'.`);
|
|
46
|
+
}
|
|
47
|
+
break;
|
|
48
|
+
case "N:N":
|
|
49
|
+
throw new Error(`Domain '${domainName}' does not support reference attributes.`);
|
|
50
|
+
default:
|
|
51
|
+
throw new Error(`Unsupported relation '${domain.relation}' for domain '${domainName}'.`);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
this.setField = () => {
|
|
55
|
+
const { name, type, config } = this.model;
|
|
56
|
+
let field;
|
|
57
|
+
switch (type) {
|
|
58
|
+
// ... existing cases for 'string', 'number', 'boolean', 'date' ...
|
|
59
|
+
case 'string':
|
|
60
|
+
field = z.string();
|
|
61
|
+
if (config.maxLength !== undefined) {
|
|
62
|
+
field = field.max(config.maxLength);
|
|
63
|
+
}
|
|
64
|
+
break;
|
|
65
|
+
case 'integer':
|
|
66
|
+
field = z.number();
|
|
67
|
+
if (typeof config.min === 'number') {
|
|
68
|
+
field = field.min(config.min);
|
|
69
|
+
}
|
|
70
|
+
if (typeof config.max === 'number') {
|
|
71
|
+
field = field.max(config.max);
|
|
72
|
+
}
|
|
73
|
+
break;
|
|
74
|
+
// case 'date':
|
|
75
|
+
// field = z.date();
|
|
76
|
+
// break;
|
|
77
|
+
case 'decimal':
|
|
78
|
+
field = z.number();
|
|
79
|
+
// min and max validation
|
|
80
|
+
if (typeof config.min === 'number') {
|
|
81
|
+
field = field.min(config.min);
|
|
82
|
+
}
|
|
83
|
+
if (typeof config.max === 'number') {
|
|
84
|
+
field = field.max(config.max);
|
|
85
|
+
}
|
|
86
|
+
// decimal precision validation (with refinement)
|
|
87
|
+
if (typeof config.precision === 'number' && config.precision >= 0) {
|
|
88
|
+
const isPrecise = (value) => {
|
|
89
|
+
if (typeof value !== 'number')
|
|
90
|
+
return true;
|
|
91
|
+
const valueAsString = value.toString();
|
|
92
|
+
const decimalPart = valueAsString.split('.')[1];
|
|
93
|
+
const decimalPlaces = decimalPart ? decimalPart.length : 0;
|
|
94
|
+
return decimalPlaces <= config.precision;
|
|
95
|
+
};
|
|
96
|
+
field = field.refine(isPrecise, `Number cannot have more than ${config.precision} decimals.`);
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
case 'boolean':
|
|
100
|
+
field = z.boolean();
|
|
101
|
+
break;
|
|
102
|
+
case "object":
|
|
103
|
+
field = z.object({});
|
|
104
|
+
break;
|
|
105
|
+
case 'enum':
|
|
106
|
+
if (!config.values || !Array.isArray(config.values) || config.values.length === 0) {
|
|
107
|
+
throw new Error(`Attribute '${name}' of type 'enum' must have a non-empty 'values' array in its config.`);
|
|
108
|
+
}
|
|
109
|
+
const enumValues = config.values.map(v => v.value);
|
|
110
|
+
field = z.enum(enumValues);
|
|
111
|
+
break;
|
|
112
|
+
case 'foreign_key':
|
|
113
|
+
if (!config.targetClass) {
|
|
114
|
+
throw new Error(`Attribute '${name}' of type 'foreign_key' is missing a 'targetClass' in its config.`);
|
|
115
|
+
}
|
|
116
|
+
const foreignClass = config.targetClass;
|
|
117
|
+
const baseSchema = z.string();
|
|
118
|
+
field = baseSchema.refine(async (documentIdOrIds) => {
|
|
119
|
+
var _b;
|
|
120
|
+
const idsToValidate = Array.isArray(documentIdOrIds) ? documentIdOrIds : [documentIdOrIds];
|
|
121
|
+
if (idsToValidate.length === 0) {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
if (this.class) {
|
|
126
|
+
const stack = this.class.getStack();
|
|
127
|
+
if (stack) {
|
|
128
|
+
const promises = idsToValidate.map(id => stack.db.get(id));
|
|
129
|
+
const fetchResult = await Promise.all(promises);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
else
|
|
133
|
+
throw new Error("Missing stack connection");
|
|
134
|
+
}
|
|
135
|
+
else
|
|
136
|
+
throw new Error("Missing class parentship");
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
if (error.status === 404) {
|
|
140
|
+
console.error(`Foreign key validation failed: document not found in class '${foreignClass}'. ${(_b = this.class) === null || _b === void 0 ? void 0 : _b.getName()}`, { error });
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}, {
|
|
146
|
+
message: `One or more documents not found in class '${foreignClass}'.`,
|
|
147
|
+
});
|
|
148
|
+
break;
|
|
149
|
+
case 'reference':
|
|
150
|
+
field = z.string().min(1);
|
|
151
|
+
break;
|
|
152
|
+
default:
|
|
153
|
+
throw new Error(`Unsupported schema type: '${type}' for field '${name}'`);
|
|
154
|
+
}
|
|
155
|
+
// These rules are applied regardless of the type, and in the correct order
|
|
156
|
+
if (config.defaultValue) {
|
|
157
|
+
field = field.default(config.defaultValue);
|
|
158
|
+
}
|
|
159
|
+
if (config.mandatory !== true) {
|
|
160
|
+
field = field.optional();
|
|
161
|
+
}
|
|
162
|
+
if (config.isArray === true) {
|
|
163
|
+
field = z.array(field);
|
|
164
|
+
}
|
|
165
|
+
this.field = field;
|
|
166
|
+
};
|
|
167
|
+
this.isPrimaryKey = () => {
|
|
168
|
+
let model = this.getModel();
|
|
169
|
+
return !!model.config.primaryKey;
|
|
170
|
+
};
|
|
171
|
+
this.isMandatory = () => {
|
|
172
|
+
return !!this.model.config.mandatory;
|
|
173
|
+
};
|
|
174
|
+
this.getModel = () => {
|
|
175
|
+
return this.model;
|
|
176
|
+
};
|
|
177
|
+
this.getClass = () => {
|
|
178
|
+
if (this.class)
|
|
179
|
+
return this.class;
|
|
180
|
+
else
|
|
181
|
+
throw Error("Missing class configuration for this attribute");
|
|
182
|
+
};
|
|
183
|
+
this.validate = async (data) => {
|
|
184
|
+
return this.field.safeParseAsync(data);
|
|
185
|
+
};
|
|
186
|
+
this.setModel = (model) => {
|
|
187
|
+
let currentModel = this.getModel();
|
|
188
|
+
model = Object.assign(currentModel || {}, model);
|
|
189
|
+
this.model = model;
|
|
190
|
+
this.defaultValue = model.config.defaultValue;
|
|
191
|
+
};
|
|
192
|
+
// TODO: Better define config
|
|
193
|
+
this.getType = (type) => {
|
|
194
|
+
if (this.checkTypeValidity(type)) {
|
|
195
|
+
return type;
|
|
196
|
+
}
|
|
197
|
+
else
|
|
198
|
+
throw Error("Invalid attribute type: " + type);
|
|
199
|
+
// return this?
|
|
200
|
+
};
|
|
201
|
+
this.getEmpty = () => {
|
|
202
|
+
// Let the field handle the default value, if present
|
|
203
|
+
// if not, it's undefined
|
|
204
|
+
const partialDoc = {
|
|
205
|
+
[this.name]: this.field.parse(undefined) || null
|
|
206
|
+
};
|
|
207
|
+
return partialDoc;
|
|
208
|
+
};
|
|
209
|
+
// getType()
|
|
210
|
+
this.getName = () => {
|
|
211
|
+
return this.name;
|
|
212
|
+
};
|
|
213
|
+
this.checkTypeValidity = (type) => {
|
|
214
|
+
let validity = false;
|
|
215
|
+
if (ATTRIBUTE_TYPES.includes(type)) {
|
|
216
|
+
validity = true;
|
|
217
|
+
}
|
|
218
|
+
return validity;
|
|
219
|
+
};
|
|
220
|
+
// TODO: change to imported const default configs for types
|
|
221
|
+
// as of now it accepts only string
|
|
222
|
+
// TODO: since config depends on attribute's type,
|
|
223
|
+
// find a way to check if given configs are correct
|
|
224
|
+
// find a way to add default configs base on type
|
|
225
|
+
this.getTypeConf = (type, config) => {
|
|
226
|
+
switch (type) {
|
|
227
|
+
// TODO: add missing cases and change values to imported const
|
|
228
|
+
case "decimal":
|
|
229
|
+
config = Object.assign({ max: null, min: null, precision: null, isArray: false }, config);
|
|
230
|
+
break;
|
|
231
|
+
case "integer":
|
|
232
|
+
config = Object.assign({ max: null, min: null, isArray: false }, config);
|
|
233
|
+
break;
|
|
234
|
+
case "string":
|
|
235
|
+
config = Object.assign({ isArray: false }, config);
|
|
236
|
+
break;
|
|
237
|
+
case "object":
|
|
238
|
+
config = Object.assign({ isArray: false }, config);
|
|
239
|
+
break;
|
|
240
|
+
case "date":
|
|
241
|
+
config = Object.assign({ format: "iso", max: null, min: null, isArray: false }, config);
|
|
242
|
+
break;
|
|
243
|
+
case "boolean":
|
|
244
|
+
config = Object.assign({ defaultValue: false, isArray: false }, config);
|
|
245
|
+
break;
|
|
246
|
+
case "foreign_key":
|
|
247
|
+
config = Object.assign({ targetClass: null, isArray: false }, config);
|
|
248
|
+
break;
|
|
249
|
+
case "enum":
|
|
250
|
+
config = Object.assign({ values: [], isArray: false }, config);
|
|
251
|
+
break;
|
|
252
|
+
case "reference":
|
|
253
|
+
config = Object.assign({ isArray: false }, config);
|
|
254
|
+
break;
|
|
255
|
+
default:
|
|
256
|
+
throw new Error("Unexpected type: " + type);
|
|
257
|
+
// return "^[a-zA-Z0-9_\\s]".concat("{0,"+config.maxLength+"}$");
|
|
258
|
+
}
|
|
259
|
+
return config;
|
|
260
|
+
};
|
|
261
|
+
this.name = name;
|
|
262
|
+
this.description = description;
|
|
263
|
+
this.setModel({
|
|
264
|
+
name: this.name,
|
|
265
|
+
description: this.description,
|
|
266
|
+
type: this.getType(type),
|
|
267
|
+
config: this.getTypeConf(type, config) || {},
|
|
268
|
+
});
|
|
269
|
+
this.setField();
|
|
270
|
+
// if it's given a class
|
|
271
|
+
// if ( classObj ) {
|
|
272
|
+
// attempt to add attribute
|
|
273
|
+
this.class = classObj;
|
|
274
|
+
// }
|
|
275
|
+
}
|
|
276
|
+
static async create(classObj, name, type, description, config) {
|
|
277
|
+
const attribute = new _a(classObj, name, type, description, config);
|
|
278
|
+
await attribute.ensureReferenceConfigIsValid();
|
|
279
|
+
await _a.build(attribute);
|
|
280
|
+
return attribute;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
_a = Attribute;
|
|
284
|
+
Attribute.build = async (attributeObj) => {
|
|
285
|
+
let classObj = attributeObj.getClass();
|
|
286
|
+
let stack = classObj.getStack();
|
|
287
|
+
if (stack) {
|
|
288
|
+
await classObj.addAttribute(attributeObj);
|
|
289
|
+
return attributeObj;
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
throw new Error("Missing db configuration");
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
export default Attribute;
|
|
296
|
+
//# sourceMappingURL=attribute.js.map
|