@objectql/core 1.1.0 → 1.2.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 (48) hide show
  1. package/CHANGELOG.md +4 -10
  2. package/dist/index.d.ts +14 -11
  3. package/dist/index.js +222 -34
  4. package/dist/index.js.map +1 -1
  5. package/dist/loader.d.ts +23 -5
  6. package/dist/loader.js +201 -9
  7. package/dist/loader.js.map +1 -1
  8. package/dist/repository.d.ts +3 -5
  9. package/dist/repository.js +107 -112
  10. package/dist/repository.js.map +1 -1
  11. package/jest.config.js +3 -0
  12. package/package.json +10 -7
  13. package/src/index.ts +261 -41
  14. package/src/loader.ts +194 -10
  15. package/src/repository.ts +123 -127
  16. package/test/action.test.ts +58 -0
  17. package/test/hook.test.ts +60 -0
  18. package/test/utils.ts +54 -0
  19. package/tsconfig.json +4 -3
  20. package/tsconfig.tsbuildinfo +1 -1
  21. package/README.md +0 -53
  22. package/dist/driver.d.ts +0 -17
  23. package/dist/driver.js +0 -3
  24. package/dist/driver.js.map +0 -1
  25. package/dist/metadata.d.ts +0 -104
  26. package/dist/metadata.js +0 -3
  27. package/dist/metadata.js.map +0 -1
  28. package/dist/query.d.ts +0 -10
  29. package/dist/query.js +0 -3
  30. package/dist/query.js.map +0 -1
  31. package/dist/registry.d.ts +0 -4
  32. package/dist/registry.js +0 -8
  33. package/dist/registry.js.map +0 -1
  34. package/dist/types.d.ts +0 -83
  35. package/dist/types.js +0 -6
  36. package/dist/types.js.map +0 -1
  37. package/src/driver.ts +0 -24
  38. package/src/metadata.ts +0 -143
  39. package/src/query.ts +0 -11
  40. package/src/registry.ts +0 -6
  41. package/src/types.ts +0 -115
  42. package/test/dynamic.test.ts +0 -34
  43. package/test/fixtures/project.action.ts +0 -6
  44. package/test/fixtures/project.object.yml +0 -41
  45. package/test/loader.test.ts +0 -22
  46. package/test/metadata.test.ts +0 -49
  47. package/test/mock-driver.ts +0 -86
  48. package/test/repository.test.ts +0 -150
@@ -1,41 +0,0 @@
1
- name: project
2
- label: Project
3
- icon: standard:case
4
- enable_search: true
5
- fields:
6
- name:
7
- label: Project Name
8
- type: text
9
- required: true
10
- searchable: true
11
- index: true
12
-
13
- status:
14
- label: Status
15
- type: select
16
- options:
17
- - label: Planned
18
- value: planned
19
- - label: In Progress
20
- value: in_progress
21
- - label: Completed
22
- value: completed
23
- defaultValue: planned
24
-
25
- start_date:
26
- label: Start Date
27
- type: date
28
-
29
- owner:
30
- label: Project Manager
31
- type: lookup
32
- reference_to: users
33
-
34
- budget:
35
- label: Total Budget
36
- type: currency
37
- scale: 2
38
-
39
- description:
40
- label: Description
41
- type: textarea
@@ -1,22 +0,0 @@
1
- import { loadObjectConfigs } from '../src/loader';
2
- import * as path from 'path';
3
-
4
- describe('Loader', () => {
5
- it('should load object configs from directory', () => {
6
- const fixturesDir = path.join(__dirname, 'fixtures');
7
- const configs = loadObjectConfigs(fixturesDir);
8
- expect(configs).toBeDefined();
9
- expect(configs['project']).toBeDefined();
10
- expect(configs['project'].name).toBe('project');
11
- expect(configs['project'].fields).toBeDefined();
12
- expect(configs['project'].fields.name).toBeDefined();
13
- });
14
-
15
- it('should load actions from .action.ts files', () => {
16
- const fixturesDir = path.join(__dirname, 'fixtures');
17
- const configs = loadObjectConfigs(fixturesDir);
18
- expect(configs['project'].actions).toBeDefined();
19
- expect(configs['project'].actions!.closeProject).toBeDefined();
20
- expect(typeof configs['project'].actions!.closeProject.handler).toBe('function');
21
- });
22
- });
@@ -1,49 +0,0 @@
1
- import { ObjectQL } from '../src/index';
2
- import { ObjectConfig } from '../src/metadata';
3
- import * as fs from 'fs';
4
- import * as path from 'path';
5
- import * as yaml from 'js-yaml';
6
-
7
- describe('Metadata Loading', () => {
8
-
9
- it('should load definitions from .object.yml file', () => {
10
- // 1. Read YAML file
11
- const yamlPath = path.join(__dirname, 'fixtures', 'project.object.yml');
12
- const fileContents = fs.readFileSync(yamlPath, 'utf8');
13
-
14
- // 2. Parse YAML
15
- const objectDef = yaml.load(fileContents) as ObjectConfig;
16
-
17
- // 3. Verify Structure
18
- expect(objectDef.name).toBe('project');
19
- expect(objectDef.fields.name.type).toBe('text');
20
- expect(objectDef.fields.status.options).toHaveLength(3);
21
- expect(objectDef.fields.budget.type).toBe('currency');
22
- expect(objectDef.fields.owner.reference_to).toBe('users');
23
-
24
- // 4. Register with ObjectQL
25
- const app = new ObjectQL({
26
- datasources: {}
27
- });
28
-
29
- app.registerObject(objectDef);
30
-
31
- // 5. Verify Registration
32
- const retrieved = app.getObject('project');
33
- expect(retrieved).toBeDefined();
34
- expect(retrieved?.label).toBe('Project');
35
- });
36
-
37
- it('should validate required properties (manual validation simulation)', () => {
38
- const yamlPath = path.join(__dirname, 'fixtures', 'project.object.yml');
39
- const fileContents = fs.readFileSync(yamlPath, 'utf8');
40
- const objectDef = yaml.load(fileContents) as ObjectConfig;
41
-
42
- function validateObject(obj: ObjectConfig) {
43
- if (!obj.name) throw new Error('Object name is required');
44
- if (!obj.fields) throw new Error('Object fields are required');
45
- }
46
-
47
- expect(() => validateObject(objectDef)).not.toThrow();
48
- });
49
- });
@@ -1,86 +0,0 @@
1
- import { Driver } from '../src/driver';
2
-
3
- export class MockDriver implements Driver {
4
- private data: Record<string, any[]> = {};
5
- private transactions: Set<any> = new Set();
6
-
7
- constructor() {}
8
-
9
- private getData(objectName: string) {
10
- if (!this.data[objectName]) {
11
- this.data[objectName] = [];
12
- }
13
- return this.data[objectName];
14
- }
15
-
16
- async find(objectName: string, query: any, options?: any): Promise<any[]> {
17
- const items = this.getData(objectName);
18
- // Very basic filter implementation for testing
19
- if (query.filters) {
20
- return items.filter(item => {
21
- // Assuming simple filter: [['field', '=', 'value']]
22
- const filter = query.filters[0];
23
- if (filter && Array.isArray(filter) && filter[1] === '=') {
24
- return item[filter[0]] === filter[2];
25
- }
26
- return true;
27
- });
28
- }
29
- return items;
30
- }
31
-
32
- async findOne(objectName: string, id: string | number, query?: any, options?: any): Promise<any> {
33
- const items = this.getData(objectName);
34
- return items.find((item: any) => item._id === id);
35
- }
36
-
37
- async create(objectName: string, data: any, options?: any): Promise<any> {
38
- const items = this.getData(objectName);
39
- const newItem = {
40
- ...data,
41
- _id: data._id || `id-${Date.now()}-${Math.random()}`
42
- };
43
- items.push(newItem);
44
- return newItem;
45
- }
46
-
47
- async update(objectName: string, id: string | number, data: any, options?: any): Promise<any> {
48
- const items = this.getData(objectName);
49
- const index = items.findIndex((item: any) => item._id === id);
50
- if (index > -1) {
51
- items[index] = { ...items[index], ...data };
52
- return items[index];
53
- }
54
- throw new Error('Not found');
55
- }
56
-
57
- async delete(objectName: string, id: string | number, options?: any): Promise<any> {
58
- const items = this.getData(objectName);
59
- const index = items.findIndex((item: any) => item._id === id);
60
- if (index > -1) {
61
- items.splice(index, 1);
62
- return true;
63
- }
64
- return false;
65
- }
66
-
67
- async count(objectName: string, filters: any, options?: any): Promise<number> {
68
- return (await this.find(objectName, { filters }, options)).length;
69
- }
70
-
71
- async beginTransaction(): Promise<any> {
72
- const trx = { id: Date.now() };
73
- this.transactions.add(trx);
74
- return trx;
75
- }
76
-
77
- async commitTransaction(trx: any): Promise<void> {
78
- if (!this.transactions.has(trx)) throw new Error('Invalid transaction');
79
- this.transactions.delete(trx);
80
- }
81
-
82
- async rollbackTransaction(trx: any): Promise<void> {
83
- if (!this.transactions.has(trx)) throw new Error('Invalid transaction');
84
- this.transactions.delete(trx);
85
- }
86
- }
@@ -1,150 +0,0 @@
1
- import { ObjectQL } from '../src/index';
2
- import { MockDriver } from './mock-driver';
3
- import { ObjectConfig } from '../src/metadata';
4
-
5
- const todoObject: ObjectConfig = {
6
- name: 'todo',
7
- fields: {
8
- title: { type: 'text' },
9
- completed: { type: 'boolean' },
10
- owner: { type: 'text' }
11
- },
12
- listeners: {}
13
- };
14
-
15
- describe('ObjectQL Repository', () => {
16
- let app: ObjectQL;
17
- let driver: MockDriver;
18
-
19
- beforeEach(() => {
20
- driver = new MockDriver();
21
- app = new ObjectQL({
22
- datasources: {
23
- default: driver
24
- },
25
- objects: {
26
- todo: todoObject
27
- }
28
- });
29
- // Reset listeners
30
- if (todoObject.listeners) {
31
- todoObject.listeners.beforeCreate = undefined;
32
- todoObject.listeners.afterCreate = undefined;
33
- }
34
- });
35
-
36
- it('should create and retrieve a record', async () => {
37
- const ctx = app.createContext({ userId: 'u1' });
38
- const repo = ctx.object('todo');
39
-
40
- const created = await repo.create({ title: 'Buy milk' });
41
- expect(created.title).toBe('Buy milk');
42
- expect(created.created_by).toBe('u1');
43
- expect(created._id).toBeDefined();
44
-
45
- const found = await repo.findOne(created._id);
46
- expect(found).toMatchObject(created);
47
- });
48
-
49
- it('should update a record', async () => {
50
- const ctx = app.createContext({ userId: 'u1' });
51
- const repo = ctx.object('todo');
52
- const created = await repo.create({ title: 'Buy milk', completed: false });
53
-
54
- const updated = await repo.update(created._id, { completed: true });
55
- expect(updated.completed).toBe(true);
56
-
57
- const found = await repo.findOne(created._id);
58
- expect(found.completed).toBe(true);
59
- });
60
-
61
- it('should delete a record', async () => {
62
- const ctx = app.createContext({ userId: 'u1' });
63
- const repo = ctx.object('todo');
64
- const created = await repo.create({ title: 'Delete me' });
65
-
66
- await repo.delete(created._id);
67
- const found = await repo.findOne(created._id);
68
- expect(found).toBeUndefined();
69
- });
70
-
71
- it('should support listeners (triggers)', async () => {
72
- const ctx = app.createContext({ userId: 'u1' });
73
- const repo = ctx.object('todo');
74
-
75
- let beforeCalled = false;
76
- let afterCalled = false;
77
-
78
- // Register listeners
79
- todoObject.listeners = {
80
- beforeCreate: async (context) => {
81
- beforeCalled = true;
82
- if (context.doc) {
83
- context.doc.title = context.doc.title + ' (checked)';
84
- }
85
- },
86
- afterCreate: async (context) => {
87
- afterCalled = true;
88
- }
89
- };
90
-
91
- const created = await repo.create({ title: 'Test hooks' });
92
-
93
- expect(beforeCalled).toBe(true);
94
- expect(afterCalled).toBe(true);
95
- expect(created.title).toBe('Test hooks (checked)');
96
- });
97
-
98
- it('should support beforeFind hook for Row Level Security', async () => {
99
- // 1. Setup data
100
- const adminCtx = app.createContext({ isSystem: true });
101
- await adminCtx.object('todo').create({ title: 'My Task', owner: 'u1' });
102
- await adminCtx.object('todo').create({ title: 'Other Task', owner: 'u2' });
103
-
104
- // 2. Setup Hook to filter by owner
105
- todoObject.listeners = {
106
- beforeFind: async (context) => {
107
- // Ignore for admin/system
108
- if (context.ctx.isSystem) return;
109
-
110
- // RLS: Only see own tasks
111
- context.utils.restrict(['owner', '=', context.ctx.userId]);
112
- }
113
- };
114
-
115
- // 3. User u1 Query
116
- const userCtx = app.createContext({ userId: 'u1' });
117
- const userResults = await userCtx.object('todo').find();
118
-
119
- expect(userResults).toHaveLength(1);
120
- expect(userResults[0].title).toBe('My Task');
121
-
122
- // 4. System Query (Bypass)
123
- const sysResults = await adminCtx.object('todo').find();
124
- expect(sysResults).toHaveLength(2);
125
- });
126
-
127
- it('should support transactions', async () => {
128
- const ctx = app.createContext({});
129
-
130
- await ctx.transaction(async (trxCtx) => {
131
- // In a real driver we would check isolation,
132
- // here we just check that the context has a transaction handle
133
- expect((trxCtx as any).transactionHandle).toBeDefined();
134
- const repo = trxCtx.object('todo');
135
- await repo.create({ title: 'Inside Trx' });
136
- });
137
-
138
- // Data should be persisted (mock driver auto-commits efficiently in memory)
139
- const found = await ctx.object('todo').find({ filters: [['title', '=', 'Inside Trx']]});
140
- expect(found).toHaveLength(1);
141
- });
142
-
143
- it('should auto-populate spaceId', async () => {
144
- const ctx = app.createContext({ spaceId: 'space-A' });
145
- const repo = ctx.object('todo');
146
-
147
- const created = await repo.create({ title: 'Space test' });
148
- expect(created.space_id).toBe('space-A');
149
- });
150
- });