fauxbase 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,608 @@
1
+ 'use strict';
2
+
3
+ // src/errors.ts
4
+ var FauxbaseError = class extends Error {
5
+ code;
6
+ details;
7
+ constructor(message, code, details) {
8
+ super(message);
9
+ this.name = "FauxbaseError";
10
+ this.code = code;
11
+ this.details = details;
12
+ }
13
+ toJSON() {
14
+ return { error: this.message, code: this.code, details: this.details };
15
+ }
16
+ };
17
+ var NotFoundError = class extends FauxbaseError {
18
+ constructor(message = "Resource not found") {
19
+ super(message, "NOT_FOUND");
20
+ this.name = "NotFoundError";
21
+ }
22
+ };
23
+ var ConflictError = class extends FauxbaseError {
24
+ constructor(message = "Resource conflict") {
25
+ super(message, "CONFLICT");
26
+ this.name = "ConflictError";
27
+ }
28
+ };
29
+ var ValidationError = class extends FauxbaseError {
30
+ constructor(message = "Validation failed", details) {
31
+ super(message, "VALIDATION", details);
32
+ this.name = "ValidationError";
33
+ }
34
+ };
35
+ var ForbiddenError = class extends FauxbaseError {
36
+ constructor(message = "Access forbidden") {
37
+ super(message, "FORBIDDEN");
38
+ this.name = "ForbiddenError";
39
+ }
40
+ };
41
+
42
+ // src/registry.ts
43
+ var fieldRegistry = /* @__PURE__ */ new Map();
44
+ var relationRegistry = /* @__PURE__ */ new Map();
45
+ var computedRegistry = /* @__PURE__ */ new Map();
46
+ var hookRegistry = /* @__PURE__ */ new Map();
47
+ function registerField(target, propertyKey, options) {
48
+ if (!fieldRegistry.has(target)) fieldRegistry.set(target, /* @__PURE__ */ new Map());
49
+ fieldRegistry.get(target).set(propertyKey, options);
50
+ }
51
+ function registerRelation(target, propertyKey, entityName) {
52
+ if (!relationRegistry.has(target)) relationRegistry.set(target, /* @__PURE__ */ new Map());
53
+ relationRegistry.get(target).set(propertyKey, entityName);
54
+ }
55
+ function registerComputed(target, propertyKey, fn) {
56
+ if (!computedRegistry.has(target)) computedRegistry.set(target, /* @__PURE__ */ new Map());
57
+ computedRegistry.get(target).set(propertyKey, fn);
58
+ }
59
+ function registerHook(target, hookType, methodName) {
60
+ if (!hookRegistry.has(target)) hookRegistry.set(target, /* @__PURE__ */ new Map());
61
+ const hooks = hookRegistry.get(target);
62
+ if (!hooks.has(hookType)) hooks.set(hookType, []);
63
+ hooks.get(hookType).push(methodName);
64
+ }
65
+ function getFieldMeta(target) {
66
+ return fieldRegistry.get(target) ?? /* @__PURE__ */ new Map();
67
+ }
68
+ function getComputedMeta(target) {
69
+ return computedRegistry.get(target) ?? /* @__PURE__ */ new Map();
70
+ }
71
+ function getHooks(target, hookType) {
72
+ return hookRegistry.get(target)?.get(hookType) ?? [];
73
+ }
74
+
75
+ // src/entity.ts
76
+ var Entity = class {
77
+ };
78
+ function field(options = {}) {
79
+ return (target, propertyKey) => {
80
+ registerField(target.constructor, propertyKey, options);
81
+ };
82
+ }
83
+ function relation(entityName) {
84
+ return (target, propertyKey) => {
85
+ registerRelation(target.constructor, propertyKey, entityName);
86
+ };
87
+ }
88
+ function computed(fn) {
89
+ return (target, propertyKey) => {
90
+ registerComputed(target.constructor, propertyKey, fn);
91
+ };
92
+ }
93
+ function applyDefaults(data, entityClass) {
94
+ const fieldMeta = getFieldMeta(entityClass);
95
+ const result = { ...data };
96
+ for (const [fieldName, options] of fieldMeta) {
97
+ if (result[fieldName] === void 0 && options.default !== void 0) {
98
+ result[fieldName] = typeof options.default === "function" ? options.default() : options.default;
99
+ }
100
+ }
101
+ return result;
102
+ }
103
+ function validateEntity(data, entityClass, isCreate) {
104
+ const fieldMeta = getFieldMeta(entityClass);
105
+ const errors = {};
106
+ for (const [fieldName, options] of fieldMeta) {
107
+ const value = data[fieldName];
108
+ if (isCreate && options.required && (value === void 0 || value === null || value === "")) {
109
+ errors[fieldName] = `${fieldName} is required`;
110
+ }
111
+ if (value !== void 0 && value !== null) {
112
+ if (options.min !== void 0 && typeof value === "number" && value < options.min) {
113
+ errors[fieldName] = `${fieldName} must be >= ${options.min}`;
114
+ }
115
+ if (options.max !== void 0 && typeof value === "number" && value > options.max) {
116
+ errors[fieldName] = `${fieldName} must be <= ${options.max}`;
117
+ }
118
+ }
119
+ }
120
+ if (Object.keys(errors).length > 0) {
121
+ throw new ValidationError("Validation failed", errors);
122
+ }
123
+ }
124
+ function applyComputedFields(data, entityClass) {
125
+ const computedMeta = getComputedMeta(entityClass);
126
+ if (computedMeta.size === 0) return data;
127
+ const result = { ...data };
128
+ for (const [key, fn] of computedMeta) {
129
+ Object.defineProperty(result, key, {
130
+ get: () => fn(result),
131
+ enumerable: true,
132
+ configurable: true
133
+ });
134
+ }
135
+ return result;
136
+ }
137
+
138
+ // src/service.ts
139
+ function beforeCreate() {
140
+ return (target, propertyKey) => {
141
+ registerHook(target.constructor, "beforeCreate", propertyKey);
142
+ };
143
+ }
144
+ function beforeUpdate() {
145
+ return (target, propertyKey) => {
146
+ registerHook(target.constructor, "beforeUpdate", propertyKey);
147
+ };
148
+ }
149
+ function afterCreate() {
150
+ return (target, propertyKey) => {
151
+ registerHook(target.constructor, "afterCreate", propertyKey);
152
+ };
153
+ }
154
+ function afterUpdate() {
155
+ return (target, propertyKey) => {
156
+ registerHook(target.constructor, "afterUpdate", propertyKey);
157
+ };
158
+ }
159
+ var Service = class {
160
+ driver;
161
+ resourceName;
162
+ /** @internal — called by createClient to wire the service */
163
+ _init(driver, resourceName) {
164
+ this.driver = driver;
165
+ this.resourceName = resourceName;
166
+ }
167
+ async list(query = {}) {
168
+ return this.driver.list(this.resourceName, query);
169
+ }
170
+ async get(id) {
171
+ return this.driver.get(this.resourceName, id);
172
+ }
173
+ async create(data) {
174
+ const allItems = (await this.driver.list(this.resourceName, {})).items;
175
+ await this.runHooks("beforeCreate", data, allItems);
176
+ const result = await this.driver.create(this.resourceName, data);
177
+ await this.runHooks("afterCreate", result.data);
178
+ return result;
179
+ }
180
+ async update(id, data) {
181
+ await this.runHooks("beforeUpdate", id, data);
182
+ const result = await this.driver.update(this.resourceName, id, data);
183
+ await this.runHooks("afterUpdate", result.data);
184
+ return result;
185
+ }
186
+ async delete(id) {
187
+ return this.driver.delete(this.resourceName, id);
188
+ }
189
+ async count(filter) {
190
+ return this.driver.count(this.resourceName, filter);
191
+ }
192
+ get bulk() {
193
+ return {
194
+ create: (items) => this.driver.bulkCreate(this.resourceName, items),
195
+ update: (updates) => this.driver.bulkUpdate(this.resourceName, updates),
196
+ delete: (ids) => this.driver.bulkDelete(this.resourceName, ids)
197
+ };
198
+ }
199
+ async runHooks(hookType, ...args) {
200
+ const methods = getHooks(this.constructor, hookType);
201
+ for (const methodName of methods) {
202
+ await this[methodName](...args);
203
+ }
204
+ }
205
+ };
206
+
207
+ // src/query-engine.ts
208
+ var OPERATORS = [
209
+ "startswith",
210
+ "endswith",
211
+ "contains",
212
+ "between",
213
+ "isnull",
214
+ "like",
215
+ "gte",
216
+ "lte",
217
+ "gt",
218
+ "lt",
219
+ "ne",
220
+ "in",
221
+ "eq"
222
+ ];
223
+ function parseFilterKey(key) {
224
+ for (const op of OPERATORS) {
225
+ const suffix = `__${op}`;
226
+ if (key.endsWith(suffix)) {
227
+ return { field: key.slice(0, -suffix.length), operator: op };
228
+ }
229
+ }
230
+ return { field: key, operator: "eq" };
231
+ }
232
+ function matchOperator(itemValue, operator, filterValue) {
233
+ if (operator === "isnull") {
234
+ return filterValue ? itemValue === null || itemValue === void 0 : itemValue !== null && itemValue !== void 0;
235
+ }
236
+ if (itemValue === null || itemValue === void 0) return false;
237
+ switch (operator) {
238
+ case "eq":
239
+ return itemValue === filterValue;
240
+ case "ne":
241
+ return itemValue !== filterValue;
242
+ case "gt":
243
+ return itemValue > filterValue;
244
+ case "gte":
245
+ return itemValue >= filterValue;
246
+ case "lt":
247
+ return itemValue < filterValue;
248
+ case "lte":
249
+ return itemValue <= filterValue;
250
+ case "like":
251
+ case "contains":
252
+ return String(itemValue).toLowerCase().includes(String(filterValue).toLowerCase());
253
+ case "startswith":
254
+ return String(itemValue).toLowerCase().startsWith(String(filterValue).toLowerCase());
255
+ case "endswith":
256
+ return String(itemValue).toLowerCase().endsWith(String(filterValue).toLowerCase());
257
+ case "between":
258
+ return Array.isArray(filterValue) && itemValue >= filterValue[0] && itemValue <= filterValue[1];
259
+ case "in":
260
+ return Array.isArray(filterValue) && filterValue.includes(itemValue);
261
+ default:
262
+ return false;
263
+ }
264
+ }
265
+ function applyFilters(items, filter) {
266
+ return items.filter((item) => {
267
+ for (const [key, value] of Object.entries(filter)) {
268
+ const { field: field2, operator } = parseFilterKey(key);
269
+ if (!matchOperator(item[field2], operator, value)) return false;
270
+ }
271
+ return true;
272
+ });
273
+ }
274
+ function applySort(items, sort) {
275
+ if (!sort) return items;
276
+ return [...items].sort((a, b) => {
277
+ const aVal = a[sort.field];
278
+ const bVal = b[sort.field];
279
+ if (aVal === bVal) return 0;
280
+ if (aVal === null || aVal === void 0) return 1;
281
+ if (bVal === null || bVal === void 0) return -1;
282
+ const cmp = aVal < bVal ? -1 : 1;
283
+ return sort.direction === "desc" ? -cmp : cmp;
284
+ });
285
+ }
286
+ function applyPagination(items, page = 1, size = 20) {
287
+ const totalItems = items.length;
288
+ const totalPages = size > 0 ? Math.ceil(totalItems / size) : 0;
289
+ const start = (page - 1) * size;
290
+ const paged = items.slice(start, start + size);
291
+ return {
292
+ items: paged,
293
+ meta: { page, size, totalItems, totalPages }
294
+ };
295
+ }
296
+ function executeQuery(items, query) {
297
+ let result = items;
298
+ result = result.filter((item) => !item.deletedAt);
299
+ if (query.filter) {
300
+ result = applyFilters(result, query.filter);
301
+ }
302
+ result = applySort(result, query.sort);
303
+ return applyPagination(result, query.page, query.size);
304
+ }
305
+
306
+ // src/drivers/local.ts
307
+ function generateUUID() {
308
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
309
+ return crypto.randomUUID();
310
+ }
311
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
312
+ const r = Math.random() * 16 | 0;
313
+ const v = c === "x" ? r : r & 3 | 8;
314
+ return v.toString(16);
315
+ });
316
+ }
317
+ var MemoryStorage = class {
318
+ data = /* @__PURE__ */ new Map();
319
+ meta = /* @__PURE__ */ new Map();
320
+ getAll(resource) {
321
+ const store = this.data.get(resource);
322
+ return store ? Array.from(store.values()) : [];
323
+ }
324
+ getById(resource, id) {
325
+ return this.data.get(resource)?.get(id);
326
+ }
327
+ set(resource, id, data) {
328
+ if (!this.data.has(resource)) this.data.set(resource, /* @__PURE__ */ new Map());
329
+ this.data.get(resource).set(id, data);
330
+ }
331
+ remove(resource, id) {
332
+ this.data.get(resource)?.delete(id);
333
+ }
334
+ clear(resource) {
335
+ this.data.delete(resource);
336
+ }
337
+ getMeta(key) {
338
+ return this.meta.get(key) ?? null;
339
+ }
340
+ setMeta(key, value) {
341
+ this.meta.set(key, value);
342
+ }
343
+ };
344
+ var LS_PREFIX = "fauxbase:";
345
+ var LS_INDEX_PREFIX = `${LS_PREFIX}__index:`;
346
+ var LS_META_PREFIX = `${LS_PREFIX}__meta:`;
347
+ var LocalStorageBackend = class {
348
+ getIndex(resource) {
349
+ const raw = localStorage.getItem(`${LS_INDEX_PREFIX}${resource}`);
350
+ return raw ? JSON.parse(raw) : [];
351
+ }
352
+ setIndex(resource, ids) {
353
+ localStorage.setItem(`${LS_INDEX_PREFIX}${resource}`, JSON.stringify(ids));
354
+ }
355
+ getAll(resource) {
356
+ const ids = this.getIndex(resource);
357
+ const items = [];
358
+ for (const id of ids) {
359
+ const raw = localStorage.getItem(`${LS_PREFIX}${resource}:${id}`);
360
+ if (raw) items.push(JSON.parse(raw));
361
+ }
362
+ return items;
363
+ }
364
+ getById(resource, id) {
365
+ const raw = localStorage.getItem(`${LS_PREFIX}${resource}:${id}`);
366
+ return raw ? JSON.parse(raw) : void 0;
367
+ }
368
+ set(resource, id, data) {
369
+ localStorage.setItem(`${LS_PREFIX}${resource}:${id}`, JSON.stringify(data));
370
+ const ids = this.getIndex(resource);
371
+ if (!ids.includes(id)) {
372
+ ids.push(id);
373
+ this.setIndex(resource, ids);
374
+ }
375
+ }
376
+ remove(resource, id) {
377
+ localStorage.removeItem(`${LS_PREFIX}${resource}:${id}`);
378
+ const ids = this.getIndex(resource);
379
+ this.setIndex(resource, ids.filter((i) => i !== id));
380
+ }
381
+ clear(resource) {
382
+ const ids = this.getIndex(resource);
383
+ for (const id of ids) {
384
+ localStorage.removeItem(`${LS_PREFIX}${resource}:${id}`);
385
+ }
386
+ localStorage.removeItem(`${LS_INDEX_PREFIX}${resource}`);
387
+ }
388
+ getMeta(key) {
389
+ return localStorage.getItem(`${LS_META_PREFIX}${key}`);
390
+ }
391
+ setMeta(key, value) {
392
+ localStorage.setItem(`${LS_META_PREFIX}${key}`, value);
393
+ }
394
+ };
395
+ var LocalDriver = class {
396
+ storage;
397
+ entityClasses = /* @__PURE__ */ new Map();
398
+ constructor(config) {
399
+ this.storage = config.persist === "localStorage" ? new LocalStorageBackend() : new MemoryStorage();
400
+ }
401
+ registerEntity(resource, entityClass) {
402
+ this.entityClasses.set(resource, entityClass);
403
+ }
404
+ async list(resource, query) {
405
+ const items = this.storage.getAll(resource);
406
+ const entityClass = this.entityClasses.get(resource);
407
+ const processed = entityClass ? items.map((item) => applyComputedFields(item, entityClass)) : items;
408
+ return executeQuery(processed, query);
409
+ }
410
+ async get(resource, id) {
411
+ const item = this.storage.getById(resource, id);
412
+ if (!item || item.deletedAt) {
413
+ throw new NotFoundError(`${resource} with id "${id}" not found`);
414
+ }
415
+ const entityClass = this.entityClasses.get(resource);
416
+ const data = entityClass ? applyComputedFields(item, entityClass) : item;
417
+ return { data };
418
+ }
419
+ async create(resource, data) {
420
+ const entityClass = this.entityClasses.get(resource);
421
+ const now = (/* @__PURE__ */ new Date()).toISOString();
422
+ let record = {
423
+ ...data,
424
+ id: data.id || generateUUID(),
425
+ createdAt: now,
426
+ updatedAt: now,
427
+ deletedAt: null,
428
+ version: 1
429
+ };
430
+ if (entityClass) {
431
+ record = applyDefaults(record, entityClass);
432
+ validateEntity(record, entityClass, true);
433
+ }
434
+ this.storage.set(resource, record.id, record);
435
+ const result = entityClass ? applyComputedFields(record, entityClass) : record;
436
+ return { data: result };
437
+ }
438
+ async update(resource, id, data) {
439
+ const existing = this.storage.getById(resource, id);
440
+ if (!existing || existing.deletedAt) {
441
+ throw new NotFoundError(`${resource} with id "${id}" not found`);
442
+ }
443
+ const entityClass = this.entityClasses.get(resource);
444
+ if (entityClass) {
445
+ validateEntity(data, entityClass, false);
446
+ }
447
+ const record = {
448
+ ...existing,
449
+ ...data,
450
+ id,
451
+ createdAt: existing.createdAt,
452
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
453
+ version: (existing.version || 0) + 1
454
+ };
455
+ this.storage.set(resource, id, record);
456
+ const result = entityClass ? applyComputedFields(record, entityClass) : record;
457
+ return { data: result };
458
+ }
459
+ async delete(resource, id) {
460
+ const existing = this.storage.getById(resource, id);
461
+ if (!existing || existing.deletedAt) {
462
+ throw new NotFoundError(`${resource} with id "${id}" not found`);
463
+ }
464
+ const now = (/* @__PURE__ */ new Date()).toISOString();
465
+ const record = {
466
+ ...existing,
467
+ deletedAt: now,
468
+ updatedAt: now,
469
+ version: (existing.version || 0) + 1
470
+ };
471
+ this.storage.set(resource, id, record);
472
+ return { data: record };
473
+ }
474
+ async count(resource, filter) {
475
+ let items = this.storage.getAll(resource).filter((item) => !item.deletedAt);
476
+ if (filter) {
477
+ items = applyFilters(items, filter);
478
+ }
479
+ return items.length;
480
+ }
481
+ async bulkCreate(resource, data) {
482
+ const results = [];
483
+ for (const item of data) {
484
+ const { data: created } = await this.create(resource, item);
485
+ results.push(created);
486
+ }
487
+ return { data: results };
488
+ }
489
+ async bulkUpdate(resource, updates) {
490
+ const results = [];
491
+ for (const { id, data } of updates) {
492
+ const { data: updated } = await this.update(resource, id, data);
493
+ results.push(updated);
494
+ }
495
+ return { data: results };
496
+ }
497
+ async bulkDelete(resource, ids) {
498
+ let count = 0;
499
+ for (const id of ids) {
500
+ await this.delete(resource, id);
501
+ count++;
502
+ }
503
+ return { data: { count } };
504
+ }
505
+ // --- Seed management (synchronous) ---
506
+ seed(resource, data, entityClass) {
507
+ for (let i = 0; i < data.length; i++) {
508
+ const seedId = `seed:${resource}:${i}`;
509
+ const now = (/* @__PURE__ */ new Date()).toISOString();
510
+ const record = applyDefaults({
511
+ ...data[i],
512
+ id: seedId,
513
+ createdAt: now,
514
+ updatedAt: now,
515
+ deletedAt: null,
516
+ version: 1
517
+ }, entityClass);
518
+ this.storage.set(resource, seedId, record);
519
+ }
520
+ }
521
+ getSeedVersion() {
522
+ return this.storage.getMeta("_seedVersion");
523
+ }
524
+ setSeedVersion(version) {
525
+ this.storage.setMeta("_seedVersion", version);
526
+ }
527
+ clear(resource) {
528
+ this.storage.clear(resource);
529
+ }
530
+ };
531
+
532
+ // src/seed.ts
533
+ function seed(entityClass, data) {
534
+ const entityName = entityClass.name.toLowerCase();
535
+ return { entityName, entityClass, data };
536
+ }
537
+ function computeSeedVersion(seeds) {
538
+ const content = JSON.stringify(
539
+ seeds.map((s) => ({ entity: s.entityName, data: s.data }))
540
+ );
541
+ return simpleHash(content);
542
+ }
543
+ function simpleHash(str) {
544
+ let hash = 0;
545
+ for (let i = 0; i < str.length; i++) {
546
+ const char = str.charCodeAt(i);
547
+ hash = (hash << 5) - hash + char;
548
+ hash = hash & hash;
549
+ }
550
+ return Math.abs(hash).toString(36);
551
+ }
552
+
553
+ // src/client.ts
554
+ function createClient(config) {
555
+ const driverConfig = config.driver ?? { type: "local" };
556
+ const driver = createDriver(driverConfig);
557
+ const client = {};
558
+ for (const [name, ServiceClass] of Object.entries(config.services)) {
559
+ const instance = new ServiceClass();
560
+ instance._init(driver, name);
561
+ if (driver instanceof LocalDriver) {
562
+ driver.registerEntity(name, instance.entity);
563
+ }
564
+ client[name] = instance;
565
+ }
566
+ if (config.seeds && driver instanceof LocalDriver) {
567
+ applySeedsIfNeeded(driver, config.seeds);
568
+ }
569
+ return client;
570
+ }
571
+ function createDriver(config) {
572
+ switch (config.type) {
573
+ case "local":
574
+ return new LocalDriver(config);
575
+ case "http":
576
+ throw new Error("HttpDriver not implemented yet");
577
+ default:
578
+ throw new Error(`Unknown driver type: ${config.type}`);
579
+ }
580
+ }
581
+ function applySeedsIfNeeded(driver, seeds) {
582
+ const newVersion = computeSeedVersion(seeds);
583
+ const currentVersion = driver.getSeedVersion();
584
+ if (currentVersion === newVersion) return;
585
+ for (const seedDef of seeds) {
586
+ driver.seed(seedDef.entityName, seedDef.data, seedDef.entityClass);
587
+ }
588
+ driver.setSeedVersion(newVersion);
589
+ }
590
+
591
+ exports.ConflictError = ConflictError;
592
+ exports.Entity = Entity;
593
+ exports.FauxbaseError = FauxbaseError;
594
+ exports.ForbiddenError = ForbiddenError;
595
+ exports.NotFoundError = NotFoundError;
596
+ exports.Service = Service;
597
+ exports.ValidationError = ValidationError;
598
+ exports.afterCreate = afterCreate;
599
+ exports.afterUpdate = afterUpdate;
600
+ exports.beforeCreate = beforeCreate;
601
+ exports.beforeUpdate = beforeUpdate;
602
+ exports.computed = computed;
603
+ exports.createClient = createClient;
604
+ exports.field = field;
605
+ exports.relation = relation;
606
+ exports.seed = seed;
607
+ //# sourceMappingURL=index.cjs.map
608
+ //# sourceMappingURL=index.cjs.map