icql-core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.js +206 -0
  2. package/package.json +32 -0
package/index.js ADDED
@@ -0,0 +1,206 @@
1
+ /**
2
+ * ICQL CORE - The Relational Engine
3
+ */
4
+
5
+ import { setDoc, getDoc, deleteDoc, initJuno } from "@junobuild/core";
6
+
7
+ export class ICQLEngine {
8
+ constructor() {
9
+ this.schemas = new Map();
10
+ this.initialized = false;
11
+ }
12
+
13
+ /**
14
+ * Initialize the Engine.
15
+ * @param {Object} config
16
+ * @param {string} config.satelliteId - The Juno Satellite ID
17
+ * @param {Array} config.schemas - Optional array of schemas to define on boot
18
+ */
19
+ async init({ satelliteId, schemas = [] }) {
20
+ if (this.initialized) return;
21
+
22
+ await initJuno({ satelliteId });
23
+
24
+ // Register initial schemas if provided
25
+ for (const schema of schemas) {
26
+ await this.defineSchema(schema);
27
+ }
28
+
29
+ this.initialized = true;
30
+ }
31
+
32
+ // --- SCHEMA MANAGER ---
33
+
34
+ async defineSchema(schema) {
35
+ const schemaKey = `schema_${schema.name}`;
36
+ let currentVersion = undefined;
37
+
38
+ try {
39
+ const existing = await getDoc({ collection: "_schemas", key: schemaKey });
40
+ if (existing) currentVersion = existing.version;
41
+ } catch (e) {}
42
+
43
+ const docPayload = { key: schemaKey, data: schema };
44
+ if (currentVersion !== undefined) docPayload.version = currentVersion;
45
+
46
+ await setDoc({ collection: "_schemas", doc: docPayload });
47
+ this.schemas.set(schema.name, schema);
48
+ return schema;
49
+ }
50
+
51
+ async getSchema(name) {
52
+ if (this.schemas.has(name)) return this.schemas.get(name);
53
+ try {
54
+ const schemaDoc = await getDoc({ collection: "_schemas", key: `schema_${name}` });
55
+ if (schemaDoc?.data) {
56
+ this.schemas.set(name, schemaDoc.data);
57
+ return schemaDoc.data;
58
+ }
59
+ } catch (e) {}
60
+ return null;
61
+ }
62
+
63
+ // --- INDEX MANAGER (Internal) ---
64
+
65
+ async _updateIndexWithRetry(indexKey, updateFn, maxAttempts = 5) {
66
+ let attempts = 0;
67
+ while (attempts < maxAttempts) {
68
+ try {
69
+ const indexDoc = await getDoc({ collection: "_indexes", key: indexKey });
70
+ let ids = [];
71
+ let currentVersion = undefined;
72
+
73
+ if (indexDoc?.data) {
74
+ ids = Array.isArray(indexDoc.data) ? indexDoc.data : (indexDoc.data.data || []);
75
+ currentVersion = indexDoc.version;
76
+ }
77
+
78
+ const newIds = await updateFn(ids);
79
+ if (newIds === null) return true;
80
+
81
+ const docPayload = { key: indexKey, data: newIds };
82
+ if (currentVersion !== undefined) docPayload.version = currentVersion;
83
+
84
+ await setDoc({ collection: "_indexes", doc: docPayload });
85
+ return true;
86
+ } catch (e) {
87
+ attempts++;
88
+ // Handle Not Found (Race condition on creation)
89
+ if (e.message?.includes('not found') || e.message?.includes('NotFound')) {
90
+ try {
91
+ const newIds = await updateFn([]);
92
+ await setDoc({ collection: "_indexes", doc: { key: indexKey, data: newIds } });
93
+ return true;
94
+ } catch (err) {}
95
+ }
96
+
97
+ // Exponential Backoff
98
+ if (attempts < maxAttempts) {
99
+ await new Promise(r => setTimeout(r, 100 * Math.pow(2, attempts - 1)));
100
+ } else {
101
+ console.error(`ICQL Index Error: Failed to update ${indexKey}`, e);
102
+ return false;
103
+ }
104
+ }
105
+ }
106
+ return false;
107
+ }
108
+
109
+ // --- CRUD OPERATIONS ---
110
+
111
+ async create(type, data) {
112
+ const docId = `doc_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
113
+ const doc = {
114
+ _id: docId,
115
+ _type: type,
116
+ _createdAt: new Date().toISOString(),
117
+ _updatedAt: new Date().toISOString(),
118
+ ...data
119
+ };
120
+
121
+ // Write to generic collection if not specific
122
+ // Note: For advanced usage, one might want specific collections.
123
+ // Here we default to 'content' unless the type matches a collection name,
124
+ // but for ICQL standard we usually assume 'post', 'author' etc exist.
125
+ // We try to write to the collection named 'type', fallback to 'content' if logic requires.
126
+ // For now, we assume the user configured the collection in Juno Config.
127
+
128
+ await setDoc({
129
+ collection: type, // We assume the collection exists (e.g. 'post')
130
+ doc: { key: docId, data: doc }
131
+ });
132
+
133
+ // Update Indexes
134
+ this._updateIndexWithRetry(`idx__type_${type}`, ids => [...new Set([...ids, docId])]);
135
+
136
+ // Auto-index slug if present
137
+ if (data.slug) {
138
+ this._updateIndexWithRetry(`idx_slug_${data.slug}`, ids => [...new Set([...ids, docId])]);
139
+ }
140
+
141
+ // Handle References (Foreign Keys)
142
+ for (const [key, value] of Object.entries(data)) {
143
+ if (value && typeof value === 'object' && value._ref) {
144
+ this._updateIndexWithRetry(`ref_reverse_${value._ref}`, ids => [...new Set([...ids, docId])]);
145
+ }
146
+ }
147
+
148
+ return doc;
149
+ }
150
+
151
+ async fetch(type) {
152
+ const indexKey = `idx__type_${type}`;
153
+ try {
154
+ const indexDoc = await getDoc({ collection: "_indexes", key: indexKey });
155
+ if (!indexDoc?.data) return [];
156
+
157
+ const docIds = Array.isArray(indexDoc.data) ? indexDoc.data : (indexDoc.data.data || []);
158
+
159
+ // Parallel Fetch
160
+ const results = await Promise.all(docIds.map(id =>
161
+ getDoc({ collection: type, key: id })
162
+ .catch(() => null) // Handle deleted docs gracefully
163
+ ));
164
+
165
+ return results.filter(r => r?.data).map(r => r.data);
166
+ } catch (e) {
167
+ return [];
168
+ }
169
+ }
170
+
171
+ async getById(docId) {
172
+ // We don't know the collection, so we try the most common ones or we need an index "all_docs".
173
+ // For this version, we require the user to know the collection OR we scan standard ones.
174
+ // A pro version would have a `sys_id_map` index.
175
+ // FALLBACK: Try 'content', 'post', 'author', 'category'
176
+ const candidates = ['post', 'author', 'category', 'content'];
177
+
178
+ for (const col of candidates) {
179
+ try {
180
+ const res = await getDoc({ collection: col, key: docId });
181
+ if (res?.data) return res.data;
182
+ } catch(e) {}
183
+ }
184
+ return null;
185
+ }
186
+
187
+ async delete(docId) {
188
+ // Same bruteforce approach for delete if we don't know the collection
189
+ const candidates = ['post', 'author', 'category', 'content'];
190
+ for (const col of candidates) {
191
+ try {
192
+ const docEnvelope = await getDoc({ collection: col, key: docId });
193
+ if (docEnvelope) {
194
+ await deleteDoc({ collection: col, doc: docEnvelope });
195
+ // Note: We are NOT cleaning up indexes here to save bandwidth/time.
196
+ // Ideally, we should remove IDs from _indexes.
197
+ return true;
198
+ }
199
+ } catch (e) {}
200
+ }
201
+ throw new Error("Document not found or could not be deleted");
202
+ }
203
+ }
204
+
205
+ // Default Singleton Instance
206
+ export const icql = new ICQLEngine();
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "icql-core",
3
+ "version": "1.0.0",
4
+ "description": "Relational Layer and Query Engine for Juno (Internet Computer). Build decentralized databases with JS only.",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "files": [
8
+ "index.js",
9
+ "README.md"
10
+ ],
11
+ "scripts": {
12
+ "test": "echo \"Error: no test specified\" && exit 1"
13
+ },
14
+ "keywords": [
15
+ "juno",
16
+ "icp",
17
+ "internet-computer",
18
+ "cms",
19
+ "relational",
20
+ "database",
21
+ "web3",
22
+ "decentralized"
23
+ ],
24
+ "author": "GrayFox",
25
+ "license": "ISC",
26
+ "peerDependencies": {
27
+ "@junobuild/core": "^0.0.57"
28
+ },
29
+ "dependencies": {
30
+ "nanoid": "^5.0.9"
31
+ }
32
+ }