@objectql/core 3.0.1 → 4.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 (96) hide show
  1. package/CHANGELOG.md +17 -3
  2. package/README.md +31 -9
  3. package/dist/ai-agent.d.ts +4 -3
  4. package/dist/ai-agent.js +10 -3
  5. package/dist/ai-agent.js.map +1 -1
  6. package/dist/app.d.ts +29 -6
  7. package/dist/app.js +117 -58
  8. package/dist/app.js.map +1 -1
  9. package/dist/formula-engine.d.ts +7 -0
  10. package/dist/formula-engine.js +9 -2
  11. package/dist/formula-engine.js.map +1 -1
  12. package/dist/formula-plugin.d.ts +52 -0
  13. package/dist/formula-plugin.js +107 -0
  14. package/dist/formula-plugin.js.map +1 -0
  15. package/dist/index.d.ts +16 -3
  16. package/dist/index.js +14 -3
  17. package/dist/index.js.map +1 -1
  18. package/dist/plugin.d.ts +89 -0
  19. package/dist/plugin.js +136 -0
  20. package/dist/plugin.js.map +1 -0
  21. package/dist/query/filter-translator.d.ts +39 -0
  22. package/dist/query/filter-translator.js +135 -0
  23. package/dist/query/filter-translator.js.map +1 -0
  24. package/dist/query/index.d.ts +22 -0
  25. package/dist/query/index.js +39 -0
  26. package/dist/query/index.js.map +1 -0
  27. package/dist/query/query-analyzer.d.ts +188 -0
  28. package/dist/query/query-analyzer.js +349 -0
  29. package/dist/query/query-analyzer.js.map +1 -0
  30. package/dist/query/query-builder.d.ts +29 -0
  31. package/dist/query/query-builder.js +71 -0
  32. package/dist/query/query-builder.js.map +1 -0
  33. package/dist/query/query-service.d.ts +152 -0
  34. package/dist/query/query-service.js +268 -0
  35. package/dist/query/query-service.js.map +1 -0
  36. package/dist/repository.d.ts +23 -2
  37. package/dist/repository.js +81 -14
  38. package/dist/repository.js.map +1 -1
  39. package/dist/util.d.ts +7 -0
  40. package/dist/util.js +18 -3
  41. package/dist/util.js.map +1 -1
  42. package/dist/validator-plugin.d.ts +56 -0
  43. package/dist/validator-plugin.js +106 -0
  44. package/dist/validator-plugin.js.map +1 -0
  45. package/dist/validator.d.ts +7 -0
  46. package/dist/validator.js +10 -8
  47. package/dist/validator.js.map +1 -1
  48. package/jest.config.js +16 -0
  49. package/package.json +7 -5
  50. package/src/ai-agent.ts +8 -0
  51. package/src/app.ts +136 -72
  52. package/src/formula-engine.ts +8 -0
  53. package/src/formula-plugin.ts +141 -0
  54. package/src/index.ts +28 -3
  55. package/src/plugin.ts +224 -0
  56. package/src/query/filter-translator.ts +148 -0
  57. package/src/query/index.ts +24 -0
  58. package/src/query/query-analyzer.ts +537 -0
  59. package/src/query/query-builder.ts +81 -0
  60. package/src/query/query-service.ts +393 -0
  61. package/src/repository.ts +101 -18
  62. package/src/util.ts +19 -3
  63. package/src/validator-plugin.ts +140 -0
  64. package/src/validator.ts +12 -5
  65. package/test/__mocks__/@objectstack/runtime.ts +255 -0
  66. package/test/app.test.ts +23 -35
  67. package/test/filter-syntax.test.ts +233 -0
  68. package/test/formula-engine.test.ts +8 -0
  69. package/test/formula-integration.test.ts +8 -0
  70. package/test/formula-plugin.test.ts +197 -0
  71. package/test/introspection.test.ts +8 -0
  72. package/test/mock-driver.ts +8 -0
  73. package/test/plugin-integration.test.ts +213 -0
  74. package/test/repository-validation.test.ts +8 -0
  75. package/test/repository.test.ts +8 -0
  76. package/test/util.test.ts +9 -1
  77. package/test/utils.ts +8 -0
  78. package/test/validator-plugin.test.ts +126 -0
  79. package/test/validator.test.ts +8 -0
  80. package/tsconfig.json +8 -0
  81. package/tsconfig.tsbuildinfo +1 -1
  82. package/dist/action.d.ts +0 -7
  83. package/dist/action.js +0 -23
  84. package/dist/action.js.map +0 -1
  85. package/dist/hook.d.ts +0 -8
  86. package/dist/hook.js +0 -25
  87. package/dist/hook.js.map +0 -1
  88. package/dist/object.d.ts +0 -3
  89. package/dist/object.js +0 -28
  90. package/dist/object.js.map +0 -1
  91. package/src/action.ts +0 -40
  92. package/src/hook.ts +0 -42
  93. package/src/object.ts +0 -26
  94. package/test/action.test.ts +0 -276
  95. package/test/hook.test.ts +0 -343
  96. package/test/object.test.ts +0 -183
package/dist/action.d.ts DELETED
@@ -1,7 +0,0 @@
1
- import { ActionContext, ActionHandler, MetadataRegistry } from '@objectql/types';
2
- export interface ActionEntry {
3
- handler: ActionHandler;
4
- packageName?: string;
5
- }
6
- export declare function registerActionHelper(actions: Record<string, ActionEntry>, objectName: string, actionName: string, handler: ActionHandler, packageName?: string): void;
7
- export declare function executeActionHelper(metadata: MetadataRegistry, runtimeActions: Record<string, ActionEntry>, objectName: string, actionName: string, ctx: ActionContext): Promise<any>;
package/dist/action.js DELETED
@@ -1,23 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.registerActionHelper = registerActionHelper;
4
- exports.executeActionHelper = executeActionHelper;
5
- function registerActionHelper(actions, objectName, actionName, handler, packageName) {
6
- const key = `${objectName}:${actionName}`;
7
- actions[key] = { handler, packageName };
8
- }
9
- async function executeActionHelper(metadata, runtimeActions, objectName, actionName, ctx) {
10
- // 1. Programmatic
11
- const key = `${objectName}:${actionName}`;
12
- const actionEntry = runtimeActions[key];
13
- if (actionEntry) {
14
- return await actionEntry.handler(ctx);
15
- }
16
- // 2. Registry (File-based)
17
- const fileActions = metadata.get('action', objectName);
18
- if (fileActions && typeof fileActions[actionName] === 'function') {
19
- return await fileActions[actionName](ctx);
20
- }
21
- throw new Error(`Action '${actionName}' not found for object '${objectName}'`);
22
- }
23
- //# sourceMappingURL=action.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"action.js","sourceRoot":"","sources":["../src/action.ts"],"names":[],"mappings":";;AAOA,oDASC;AAED,kDAqBC;AAhCD,SAAgB,oBAAoB,CAChC,OAAoC,EACpC,UAAkB,EAClB,UAAkB,EAClB,OAAsB,EACtB,WAAoB;IAEpB,MAAM,GAAG,GAAG,GAAG,UAAU,IAAI,UAAU,EAAE,CAAC;IAC1C,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC;AAC5C,CAAC;AAEM,KAAK,UAAU,mBAAmB,CACrC,QAA0B,EAC1B,cAA2C,EAC3C,UAAkB,EAClB,UAAkB,EAClB,GAAkB;IAElB,kBAAkB;IAClB,MAAM,GAAG,GAAG,GAAG,UAAU,IAAI,UAAU,EAAE,CAAC;IAC1C,MAAM,WAAW,GAAG,cAAc,CAAC,GAAG,CAAC,CAAC;IACxC,IAAI,WAAW,EAAE,CAAC;QACd,OAAO,MAAM,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC1C,CAAC;IAED,2BAA2B;IAC3B,MAAM,WAAW,GAAG,QAAQ,CAAC,GAAG,CAAM,QAAQ,EAAE,UAAU,CAAC,CAAC;IAC5D,IAAI,WAAW,IAAI,OAAO,WAAW,CAAC,UAAU,CAAC,KAAK,UAAU,EAAE,CAAC;QAC/D,OAAO,MAAM,WAAW,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,CAAC;IAC9C,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,WAAW,UAAU,2BAA2B,UAAU,GAAG,CAAC,CAAC;AACnF,CAAC"}
package/dist/hook.d.ts DELETED
@@ -1,8 +0,0 @@
1
- import { HookContext, HookHandler, HookName, MetadataRegistry } from '@objectql/types';
2
- export interface HookEntry {
3
- objectName: string;
4
- handler: HookHandler;
5
- packageName?: string;
6
- }
7
- export declare function registerHookHelper(hooks: Record<string, HookEntry[]>, event: HookName, objectName: string, handler: HookHandler, packageName?: string): void;
8
- export declare function triggerHookHelper(metadata: MetadataRegistry, runtimeHooks: Record<string, HookEntry[]>, event: HookName, objectName: string, ctx: HookContext): Promise<void>;
package/dist/hook.js DELETED
@@ -1,25 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.registerHookHelper = registerHookHelper;
4
- exports.triggerHookHelper = triggerHookHelper;
5
- function registerHookHelper(hooks, event, objectName, handler, packageName) {
6
- if (!hooks[event]) {
7
- hooks[event] = [];
8
- }
9
- hooks[event].push({ objectName, handler, packageName });
10
- }
11
- async function triggerHookHelper(metadata, runtimeHooks, event, objectName, ctx) {
12
- // 1. Registry Hooks (File-based)
13
- const fileHooks = metadata.get('hook', objectName);
14
- if (fileHooks && typeof fileHooks[event] === 'function') {
15
- await fileHooks[event](ctx);
16
- }
17
- // 2. Programmatic Hooks
18
- const hooks = runtimeHooks[event] || [];
19
- for (const hook of hooks) {
20
- if (hook.objectName === '*' || hook.objectName === objectName) {
21
- await hook.handler(ctx);
22
- }
23
- }
24
- }
25
- //# sourceMappingURL=hook.js.map
package/dist/hook.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"hook.js","sourceRoot":"","sources":["../src/hook.ts"],"names":[],"mappings":";;AAQA,gDAWC;AAED,8CAoBC;AAjCD,SAAgB,kBAAkB,CAC9B,KAAkC,EAClC,KAAe,EACf,UAAkB,EAClB,OAAoB,EACpB,WAAoB;IAEpB,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;QAChB,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC;IACtB,CAAC;IACD,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC,CAAC;AAC5D,CAAC;AAEM,KAAK,UAAU,iBAAiB,CACnC,QAA0B,EAC1B,YAAyC,EACzC,KAAe,EACf,UAAkB,EAClB,GAAgB;IAEhB,iCAAiC;IACjC,MAAM,SAAS,GAAG,QAAQ,CAAC,GAAG,CAAM,MAAM,EAAE,UAAU,CAAC,CAAC;IACxD,IAAI,SAAS,IAAI,OAAO,SAAS,CAAC,KAAK,CAAC,KAAK,UAAU,EAAE,CAAC;QACtD,MAAM,SAAS,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC;IAChC,CAAC;IAED,wBAAwB;IACxB,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;IACxC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACvB,IAAI,IAAI,CAAC,UAAU,KAAK,GAAG,IAAI,IAAI,CAAC,UAAU,KAAK,UAAU,EAAE,CAAC;YAC5D,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC5B,CAAC;IACL,CAAC;AACL,CAAC"}
package/dist/object.d.ts DELETED
@@ -1,3 +0,0 @@
1
- import { ObjectConfig, MetadataRegistry } from '@objectql/types';
2
- export declare function registerObjectHelper(metadata: MetadataRegistry, object: ObjectConfig): void;
3
- export declare function getConfigsHelper(metadata: MetadataRegistry): Record<string, ObjectConfig>;
package/dist/object.js DELETED
@@ -1,28 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.registerObjectHelper = registerObjectHelper;
4
- exports.getConfigsHelper = getConfigsHelper;
5
- function registerObjectHelper(metadata, object) {
6
- // Normalize fields
7
- if (object.fields) {
8
- for (const [key, field] of Object.entries(object.fields)) {
9
- if (!field.name) {
10
- field.name = key;
11
- }
12
- }
13
- }
14
- metadata.register('object', {
15
- type: 'object',
16
- id: object.name,
17
- content: object
18
- });
19
- }
20
- function getConfigsHelper(metadata) {
21
- const result = {};
22
- const objects = metadata.list('object');
23
- for (const obj of objects) {
24
- result[obj.name] = obj;
25
- }
26
- return result;
27
- }
28
- //# sourceMappingURL=object.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"object.js","sourceRoot":"","sources":["../src/object.ts"],"names":[],"mappings":";;AAEA,oDAcC;AAED,4CAOC;AAvBD,SAAgB,oBAAoB,CAAC,QAA0B,EAAE,MAAoB;IACjF,mBAAmB;IACnB,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;QAChB,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;YACvD,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;gBACd,KAAK,CAAC,IAAI,GAAG,GAAG,CAAC;YACrB,CAAC;QACL,CAAC;IACL,CAAC;IACD,QAAQ,CAAC,QAAQ,CAAC,QAAQ,EAAE;QACxB,IAAI,EAAE,QAAQ;QACd,EAAE,EAAE,MAAM,CAAC,IAAI;QACf,OAAO,EAAE,MAAM;KAClB,CAAC,CAAC;AACP,CAAC;AAED,SAAgB,gBAAgB,CAAC,QAA0B;IACvD,MAAM,MAAM,GAAiC,EAAE,CAAC;IAChD,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAe,QAAQ,CAAC,CAAC;IACtD,KAAK,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;QACxB,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC;IAC3B,CAAC;IACD,OAAO,MAAM,CAAC;AAClB,CAAC"}
package/src/action.ts DELETED
@@ -1,40 +0,0 @@
1
- import { ActionContext, ActionHandler, MetadataRegistry } from '@objectql/types';
2
-
3
- export interface ActionEntry {
4
- handler: ActionHandler;
5
- packageName?: string;
6
- }
7
-
8
- export function registerActionHelper(
9
- actions: Record<string, ActionEntry>,
10
- objectName: string,
11
- actionName: string,
12
- handler: ActionHandler,
13
- packageName?: string
14
- ) {
15
- const key = `${objectName}:${actionName}`;
16
- actions[key] = { handler, packageName };
17
- }
18
-
19
- export async function executeActionHelper(
20
- metadata: MetadataRegistry,
21
- runtimeActions: Record<string, ActionEntry>,
22
- objectName: string,
23
- actionName: string,
24
- ctx: ActionContext
25
- ) {
26
- // 1. Programmatic
27
- const key = `${objectName}:${actionName}`;
28
- const actionEntry = runtimeActions[key];
29
- if (actionEntry) {
30
- return await actionEntry.handler(ctx);
31
- }
32
-
33
- // 2. Registry (File-based)
34
- const fileActions = metadata.get<any>('action', objectName);
35
- if (fileActions && typeof fileActions[actionName] === 'function') {
36
- return await fileActions[actionName](ctx);
37
- }
38
-
39
- throw new Error(`Action '${actionName}' not found for object '${objectName}'`);
40
- }
package/src/hook.ts DELETED
@@ -1,42 +0,0 @@
1
- import { HookContext, HookHandler, HookName, MetadataRegistry } from '@objectql/types';
2
-
3
- export interface HookEntry {
4
- objectName: string;
5
- handler: HookHandler;
6
- packageName?: string;
7
- }
8
-
9
- export function registerHookHelper(
10
- hooks: Record<string, HookEntry[]>,
11
- event: HookName,
12
- objectName: string,
13
- handler: HookHandler,
14
- packageName?: string
15
- ) {
16
- if (!hooks[event]) {
17
- hooks[event] = [];
18
- }
19
- hooks[event].push({ objectName, handler, packageName });
20
- }
21
-
22
- export async function triggerHookHelper(
23
- metadata: MetadataRegistry,
24
- runtimeHooks: Record<string, HookEntry[]>,
25
- event: HookName,
26
- objectName: string,
27
- ctx: HookContext
28
- ) {
29
- // 1. Registry Hooks (File-based)
30
- const fileHooks = metadata.get<any>('hook', objectName);
31
- if (fileHooks && typeof fileHooks[event] === 'function') {
32
- await fileHooks[event](ctx);
33
- }
34
-
35
- // 2. Programmatic Hooks
36
- const hooks = runtimeHooks[event] || [];
37
- for (const hook of hooks) {
38
- if (hook.objectName === '*' || hook.objectName === objectName) {
39
- await hook.handler(ctx);
40
- }
41
- }
42
- }
package/src/object.ts DELETED
@@ -1,26 +0,0 @@
1
- import { ObjectConfig, MetadataRegistry } from '@objectql/types';
2
-
3
- export function registerObjectHelper(metadata: MetadataRegistry, object: ObjectConfig) {
4
- // Normalize fields
5
- if (object.fields) {
6
- for (const [key, field] of Object.entries(object.fields)) {
7
- if (!field.name) {
8
- field.name = key;
9
- }
10
- }
11
- }
12
- metadata.register('object', {
13
- type: 'object',
14
- id: object.name,
15
- content: object
16
- });
17
- }
18
-
19
- export function getConfigsHelper(metadata: MetadataRegistry): Record<string, ObjectConfig> {
20
- const result: Record<string, ObjectConfig> = {};
21
- const objects = metadata.list<ObjectConfig>('object');
22
- for (const obj of objects) {
23
- result[obj.name] = obj;
24
- }
25
- return result;
26
- }
@@ -1,276 +0,0 @@
1
- import { ObjectQL } from '../src';
2
- import { MockDriver } from './mock-driver';
3
-
4
- describe('ObjectQL Actions', () => {
5
- let app: ObjectQL;
6
- let driver: MockDriver;
7
-
8
- beforeEach(async () => {
9
- driver = new MockDriver();
10
- app = new ObjectQL({
11
- datasources: {
12
- default: driver
13
- },
14
- objects: {
15
- 'invoice': {
16
- name: 'invoice',
17
- fields: {
18
- amount: { type: 'number' },
19
- status: { type: 'text' },
20
- paid_amount: { type: 'number' }
21
- },
22
- actions: {
23
- 'pay': {
24
- type: 'record',
25
- label: 'Pay Invoice',
26
- params: {
27
- method: { type: 'text' }
28
- }
29
- },
30
- 'import_invoices': {
31
- type: 'global',
32
- label: 'Import Invoices',
33
- params: {
34
- source: { type: 'text' }
35
- }
36
- }
37
- }
38
- }
39
- }
40
- });
41
- await app.init();
42
- });
43
-
44
- describe('Record Actions', () => {
45
- it('should execute record action with id parameter', async () => {
46
- const repo = app.createContext({}).object('invoice');
47
-
48
- // Create an invoice first
49
- const invoice = await repo.create({ amount: 1000, status: 'pending' });
50
-
51
- let actionCalled = false;
52
- app.registerAction('invoice', 'pay', async (ctx) => {
53
- actionCalled = true;
54
- expect(ctx.objectName).toBe('invoice');
55
- expect(ctx.actionName).toBe('pay');
56
- expect(ctx.id).toBe(invoice._id);
57
- expect(ctx.input.method).toBe('credit_card');
58
-
59
- // Update the invoice status
60
- await ctx.api.update('invoice', ctx.id!, {
61
- status: 'paid',
62
- paid_amount: ctx.input.amount || 1000
63
- });
64
-
65
- return { success: true, paid: true };
66
- });
67
-
68
- const result = await repo.execute('pay', invoice._id, { method: 'credit_card', amount: 1000 });
69
-
70
- expect(actionCalled).toBe(true);
71
- expect(result.success).toBe(true);
72
- expect(result.paid).toBe(true);
73
- });
74
-
75
- it('should provide access to record data via api', async () => {
76
- const repo = app.createContext({}).object('invoice');
77
-
78
- const invoice = await repo.create({ amount: 500, status: 'pending' });
79
-
80
- app.registerAction('invoice', 'pay', async (ctx) => {
81
- // Fetch current record
82
- const current = await ctx.api.findOne('invoice', ctx.id!);
83
- expect(current).toBeDefined();
84
- expect(current.amount).toBe(500);
85
-
86
- return { currentAmount: current.amount };
87
- });
88
-
89
- const result = await repo.execute('pay', invoice._id, { method: 'cash' });
90
- expect(result.currentAmount).toBe(500);
91
- });
92
-
93
- it('should validate business rules in record action', async () => {
94
- const repo = app.createContext({}).object('invoice');
95
-
96
- const invoice = await repo.create({ amount: 1000, status: 'paid' });
97
-
98
- app.registerAction('invoice', 'pay', async (ctx) => {
99
- const current = await ctx.api.findOne('invoice', ctx.id!);
100
- if (current.status === 'paid') {
101
- throw new Error('Invoice is already paid');
102
- }
103
- return { success: true };
104
- });
105
-
106
- await expect(repo.execute('pay', invoice._id, { method: 'credit_card' }))
107
- .rejects
108
- .toThrow('Invoice is already paid');
109
- });
110
-
111
- it('should provide user context in action', async () => {
112
- const repo = app.createContext({ userId: 'user123', userName: 'John Doe' }).object('invoice');
113
-
114
- const invoice = await repo.create({ amount: 100, status: 'pending' });
115
-
116
- let capturedUser: any;
117
- app.registerAction('invoice', 'pay', async (ctx) => {
118
- capturedUser = ctx.user;
119
- return { success: true };
120
- });
121
-
122
- await repo.execute('pay', invoice._id, { method: 'cash' });
123
-
124
- expect(capturedUser).toBeDefined();
125
- expect(capturedUser.id).toBe('user123');
126
- });
127
- });
128
-
129
- describe('Global Actions', () => {
130
- it('should execute global action without id parameter', async () => {
131
- const repo = app.createContext({}).object('invoice');
132
-
133
- let actionCalled = false;
134
- app.registerAction('invoice', 'import_invoices', async (ctx) => {
135
- actionCalled = true;
136
- expect(ctx.objectName).toBe('invoice');
137
- expect(ctx.actionName).toBe('import_invoices');
138
- expect(ctx.id).toBeUndefined();
139
- expect(ctx.input.source).toBe('external_api');
140
-
141
- // Create multiple records
142
- await ctx.api.create('invoice', { amount: 100, status: 'pending' });
143
- await ctx.api.create('invoice', { amount: 200, status: 'pending' });
144
-
145
- return { imported: 2 };
146
- });
147
-
148
- const result = await repo.execute('import_invoices', undefined, { source: 'external_api' });
149
-
150
- expect(actionCalled).toBe(true);
151
- expect(result.imported).toBe(2);
152
- });
153
-
154
- it('should perform batch operations in global action', async () => {
155
- const repo = app.createContext({}).object('invoice');
156
-
157
- // Create some test invoices
158
- await repo.create({ amount: 100, status: 'pending' });
159
- await repo.create({ amount: 200, status: 'pending' });
160
- await repo.create({ amount: 300, status: 'paid' });
161
-
162
- app.registerAction('invoice', 'import_invoices', async (ctx) => {
163
- // Count pending invoices
164
- const count = await ctx.api.count('invoice', {
165
- filters: [['status', '=', 'pending']]
166
- });
167
-
168
- return { pendingCount: count };
169
- });
170
-
171
- const result = await repo.execute('import_invoices', undefined, { source: 'test' });
172
- expect(result.pendingCount).toBe(2);
173
- });
174
- });
175
-
176
- describe('Action Input Validation', () => {
177
- it('should receive validated input parameters', async () => {
178
- const repo = app.createContext({}).object('invoice');
179
-
180
- const invoice = await repo.create({ amount: 1000, status: 'pending' });
181
-
182
- app.registerAction('invoice', 'pay', async (ctx) => {
183
- // Input should match the params defined in action config
184
- expect(ctx.input).toBeDefined();
185
- expect(typeof ctx.input.method).toBe('string');
186
-
187
- return { method: ctx.input.method };
188
- });
189
-
190
- const result = await repo.execute('pay', invoice._id, { method: 'bank_transfer' });
191
- expect(result.method).toBe('bank_transfer');
192
- });
193
-
194
- it('should handle missing optional parameters', async () => {
195
- const repo = app.createContext({}).object('invoice');
196
-
197
- const invoice = await repo.create({ amount: 1000, status: 'pending' });
198
-
199
- app.registerAction('invoice', 'pay', async (ctx) => {
200
- // Optional parameters might be undefined
201
- const comment = ctx.input.comment || 'No comment';
202
- return { comment };
203
- });
204
-
205
- const result = await repo.execute('pay', invoice._id, { method: 'cash' });
206
- expect(result.comment).toBe('No comment');
207
- });
208
- });
209
-
210
- describe('Error Handling', () => {
211
- it('should throw error if action not registered', async () => {
212
- const repo = app.createContext({}).object('invoice');
213
- await expect(repo.execute('refund', '1', {}))
214
- .rejects
215
- .toThrow("Action 'refund' not found for object 'invoice'");
216
- });
217
-
218
- it('should propagate errors from action handler', async () => {
219
- const repo = app.createContext({}).object('invoice');
220
-
221
- const invoice = await repo.create({ amount: 1000, status: 'pending' });
222
-
223
- app.registerAction('invoice', 'pay', async (ctx) => {
224
- throw new Error('Payment gateway is down');
225
- });
226
-
227
- await expect(repo.execute('pay', invoice._id, { method: 'credit_card' }))
228
- .rejects
229
- .toThrow('Payment gateway is down');
230
- });
231
- });
232
-
233
- describe('Complex Action Workflows', () => {
234
- it('should perform multi-step operations in action', async () => {
235
- const repo = app.createContext({}).object('invoice');
236
-
237
- const invoice = await repo.create({ amount: 1000, status: 'pending', paid_amount: 0 });
238
-
239
- app.registerAction('invoice', 'pay', async (ctx) => {
240
- // Step 1: Fetch current state
241
- const current = await ctx.api.findOne('invoice', ctx.id!);
242
-
243
- // Step 2: Validate
244
- if (current.status === 'paid') {
245
- throw new Error('Already paid');
246
- }
247
-
248
- // Step 3: Update invoice
249
- await ctx.api.update('invoice', ctx.id!, {
250
- status: 'paid',
251
- paid_amount: current.amount
252
- });
253
-
254
- // Step 4: Could create related records (e.g., payment record)
255
- // await ctx.api.create('payment', { ... });
256
-
257
- return {
258
- success: true,
259
- amount: current.amount,
260
- newStatus: 'paid'
261
- };
262
- });
263
-
264
- const result = await repo.execute('pay', invoice._id, { method: 'credit_card' });
265
-
266
- expect(result.success).toBe(true);
267
- expect(result.amount).toBe(1000);
268
- expect(result.newStatus).toBe('paid');
269
-
270
- // Verify the update
271
- const updated = await repo.findOne(invoice._id);
272
- expect(updated.status).toBe('paid');
273
- expect(updated.paid_amount).toBe(1000);
274
- });
275
- });
276
- });