@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.
Files changed (77) hide show
  1. package/README.md +214 -0
  2. package/lib/core/attribute.d.ts +174 -0
  3. package/lib/core/attribute.js +296 -0
  4. package/lib/core/attribute.js.map +1 -0
  5. package/lib/core/class.d.ts +340 -0
  6. package/lib/core/class.js +540 -0
  7. package/lib/core/class.js.map +1 -0
  8. package/lib/core/crypto-engine/index.d.ts +121 -0
  9. package/lib/core/crypto-engine/index.js +130 -0
  10. package/lib/core/crypto-engine/index.js.map +1 -0
  11. package/lib/core/crypto-engine/utils.d.ts +20 -0
  12. package/lib/core/crypto-engine/utils.js +76 -0
  13. package/lib/core/crypto-engine/utils.js.map +1 -0
  14. package/lib/core/datamodel/index.d.ts +3 -0
  15. package/lib/core/datamodel/index.js +1186 -0
  16. package/lib/core/datamodel/index.js.map +1 -0
  17. package/lib/core/domain.d.ts +200 -0
  18. package/lib/core/domain.js +285 -0
  19. package/lib/core/domain.js.map +1 -0
  20. package/lib/core/index.d.ts +176 -0
  21. package/lib/core/index.js +379 -0
  22. package/lib/core/index.js.map +1 -0
  23. package/lib/core/job-engine/index.d.ts +110 -0
  24. package/lib/core/job-engine/index.js +116 -0
  25. package/lib/core/job-engine/index.js.map +1 -0
  26. package/lib/core/policy-engine/index.d.ts +97 -0
  27. package/lib/core/policy-engine/index.js +150 -0
  28. package/lib/core/policy-engine/index.js.map +1 -0
  29. package/lib/core/query-engine/accumulators.d.ts +9 -0
  30. package/lib/core/query-engine/accumulators.js +258 -0
  31. package/lib/core/query-engine/accumulators.js.map +1 -0
  32. package/lib/core/query-engine/evaluator.d.ts +37 -0
  33. package/lib/core/query-engine/evaluator.js +179 -0
  34. package/lib/core/query-engine/evaluator.js.map +1 -0
  35. package/lib/core/query-engine/executor.d.ts +14 -0
  36. package/lib/core/query-engine/executor.js +405 -0
  37. package/lib/core/query-engine/executor.js.map +1 -0
  38. package/lib/core/query-engine/index.d.ts +4 -0
  39. package/lib/core/query-engine/index.js +4 -0
  40. package/lib/core/query-engine/index.js.map +1 -0
  41. package/lib/core/query-engine/parser.d.ts +10 -0
  42. package/lib/core/query-engine/parser.js +515 -0
  43. package/lib/core/query-engine/parser.js.map +1 -0
  44. package/lib/core/query-engine/planner.d.ts +27 -0
  45. package/lib/core/query-engine/planner.js +330 -0
  46. package/lib/core/query-engine/planner.js.map +1 -0
  47. package/lib/core/stack.d.ts +497 -0
  48. package/lib/core/stack.js +1507 -0
  49. package/lib/core/stack.js.map +1 -0
  50. package/lib/core/test-utils/docstack.d.ts +29 -0
  51. package/lib/core/test-utils/docstack.js +222 -0
  52. package/lib/core/test-utils/docstack.js.map +1 -0
  53. package/lib/core/trigger/index.d.ts +31 -0
  54. package/lib/core/trigger/index.js +81 -0
  55. package/lib/core/trigger/index.js.map +1 -0
  56. package/lib/index.d.ts +4 -0
  57. package/lib/index.js +8237 -0
  58. package/lib/index.js.map +1 -0
  59. package/lib/plugins/pouchdb.d.ts +9 -0
  60. package/lib/plugins/pouchdb.js +403 -0
  61. package/lib/plugins/pouchdb.js.map +1 -0
  62. package/lib/utils/crypto/index.d.ts +3 -0
  63. package/lib/utils/crypto/index.js +34 -0
  64. package/lib/utils/crypto/index.js.map +1 -0
  65. package/lib/utils/index.d.ts +4 -0
  66. package/lib/utils/index.js +58 -0
  67. package/lib/utils/index.js.map +1 -0
  68. package/lib/utils/logger/index.d.ts +4 -0
  69. package/lib/utils/logger/index.js +20 -0
  70. package/lib/utils/logger/index.js.map +1 -0
  71. package/lib/utils/logger/transport.d.ts +11 -0
  72. package/lib/utils/logger/transport.js +28 -0
  73. package/lib/utils/logger/transport.js.map +1 -0
  74. package/lib/workers/dataModel.d.ts +1 -0
  75. package/lib/workers/dataModel.js +48 -0
  76. package/lib/workers/dataModel.js.map +1 -0
  77. package/package.json +60 -0
package/README.md ADDED
@@ -0,0 +1,214 @@
1
+ [![Donate](https://img.shields.io/badge/Donate-PayPal-blue.svg)](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