@aloma.io/integration-sdk 3.8.68 → 3.8.69

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 (34) hide show
  1. package/build/transform/index.d.mts +2 -2
  2. package/build/transform/index.mjs +493 -78
  3. package/package.json +2 -2
  4. package/src/transform/index.mts +530 -96
  5. package/test/fixtures/transform/basic/expected.json +9 -0
  6. package/test/fixtures/transform/basic/src/controller/abstract.mts +5 -0
  7. package/test/fixtures/transform/basic/src/controller/index.mts +30 -0
  8. package/test/fixtures/transform/basic/tsconfig.json +1 -0
  9. package/test/fixtures/transform/interface-param/src/controller/abstract.mts +5 -0
  10. package/test/fixtures/transform/interface-param/src/controller/index.mts +12 -0
  11. package/test/fixtures/transform/interface-param/tsconfig.json +1 -0
  12. package/test/fixtures/transform/overloads/expected.json +6 -0
  13. package/test/fixtures/transform/overloads/src/controller/abstract.mts +5 -0
  14. package/test/fixtures/transform/overloads/src/controller/index.mts +11 -0
  15. package/test/fixtures/transform/overloads/tsconfig.json +1 -0
  16. package/test/fixtures/transform/tdd-stub/expected.json +8 -0
  17. package/test/fixtures/transform/tdd-stub/src/controller/abstract.mts +6 -0
  18. package/test/fixtures/transform/tdd-stub/src/controller/index.mts +204 -0
  19. package/test/fixtures/transform/tdd-stub/tsconfig.json +1 -0
  20. package/test/fixtures/transform/two-exports/src/controller/abstract.mts +5 -0
  21. package/test/fixtures/transform/two-exports/src/controller/index.mts +9 -0
  22. package/test/fixtures/transform/two-exports/tsconfig.json +1 -0
  23. package/test/fixtures/transform/type-error/src/controller/abstract.mts +5 -0
  24. package/test/fixtures/transform/type-error/src/controller/index.mts +7 -0
  25. package/test/fixtures/transform/type-error/tsconfig.json +1 -0
  26. package/test/fixtures/transform/typed/expected.json +8 -0
  27. package/test/fixtures/transform/typed/src/controller/abstract.mts +5 -0
  28. package/test/fixtures/transform/typed/src/controller/index.mts +13 -0
  29. package/test/fixtures/transform/typed/tsconfig.json +1 -0
  30. package/test/fixtures/transform/visibility/expected.json +9 -0
  31. package/test/fixtures/transform/visibility/src/controller/abstract.mts +5 -0
  32. package/test/fixtures/transform/visibility/src/controller/index.mts +19 -0
  33. package/test/fixtures/transform/visibility/tsconfig.json +1 -0
  34. package/test/transform.test.mjs +68 -0
@@ -0,0 +1,30 @@
1
+ import {AbstractController} from './abstract.mjs';
2
+
3
+ export default class Controller extends AbstractController {
4
+ /**
5
+ * greet somebody
6
+ * @param name - who to greet, e.g. Ann
7
+ * @param times - how often
8
+ * @param opts - extra \@options
9
+ * @namespace demo
10
+ *
11
+ * @example
12
+ * ```javascript
13
+ * greet({name: 'Ann'})
14
+ * ```
15
+ */
16
+ async greet({name, times = 2, loud = false, label = 'x', neg = -1, nothing = null}, other = 1) {
17
+ return {ok: true, greeting: name + times};
18
+ }
19
+
20
+ /** no params */
21
+ async ping() {
22
+ return {ok: true};
23
+ }
24
+
25
+ async list(): Promise<string[]> {
26
+ return [];
27
+ }
28
+
29
+ async nothing(): Promise<void> {}
30
+ }
@@ -0,0 +1 @@
1
+ {"compilerOptions":{"target":"es2022","module":"node16","moduleResolution":"node16","skipLibCheck":true,"strict":true,"noImplicitAny":false,"experimentalDecorators":true,"esModuleInterop":true},"include":["src/**/*"]}
@@ -0,0 +1,5 @@
1
+ // stands in for AbstractController from @aloma.io/integration-sdk
2
+ export class AbstractController {
3
+ protected config: any = {};
4
+ async inheritedPublic(): Promise<void> {}
5
+ }
@@ -0,0 +1,12 @@
1
+ import {AbstractController} from './abstract.mjs';
2
+
3
+ interface Opts {
4
+ id: string;
5
+ n?: number;
6
+ }
7
+
8
+ export default class Controller extends AbstractController {
9
+ async viaInterface({id, n}: Opts): Promise<{id: string; n: number | undefined}> {
10
+ return {id, n};
11
+ }
12
+ }
@@ -0,0 +1 @@
1
+ {"compilerOptions":{"target":"es2022","module":"node16","moduleResolution":"node16","skipLibCheck":true,"strict":true,"noImplicitAny":false,"experimentalDecorators":true,"esModuleInterop":true},"include":["src/**/*"]}
@@ -0,0 +1,6 @@
1
+ {
2
+ "text": "\n/**\n * first form\n *\n * \n * \n **/ \ndeclare function over({a}: {\n/**\n\n */\n a: string}): string;\n \n\n/**\n * second form\n *\n * \n * \n **/ \ndeclare function over({b}: {\n/**\n\n */\n b: number}): number;\n ",
3
+ "methods": [
4
+ "over"
5
+ ]
6
+ }
@@ -0,0 +1,5 @@
1
+ // stands in for AbstractController from @aloma.io/integration-sdk
2
+ export class AbstractController {
3
+ protected config: any = {};
4
+ async inheritedPublic(): Promise<void> {}
5
+ }
@@ -0,0 +1,11 @@
1
+ import {AbstractController} from './abstract.mjs';
2
+
3
+ export default class Controller extends AbstractController {
4
+ /** first form */
5
+ async over({a}: {a: string}): Promise<string>;
6
+ /** second form */
7
+ async over({b}: {b: number}): Promise<number>;
8
+ async over(args: any): Promise<any> {
9
+ return args;
10
+ }
11
+ }
@@ -0,0 +1 @@
1
+ {"compilerOptions":{"target":"es2022","module":"node16","moduleResolution":"node16","skipLibCheck":true,"strict":true,"noImplicitAny":false,"experimentalDecorators":true,"esModuleInterop":true},"include":["src/**/*"]}
@@ -0,0 +1,8 @@
1
+ {
2
+ "text": "\n/**\n * Returns recorded calls scoped to the current task.\n *\n * \n * @example usage\n```javascript\nconst log = await connectors.testing.getCallLog({})\n// log.calls = [{method, args, timestamp}, ...] for THIS task only\n```\n **/ \ndeclare function getCallLog(): { calls: CallRecord[]; count: number; };\n \n/**\n * Resets the call log for the current task only.\n *\n * \n * @example usage\n```javascript\nawait connectors.testing.clearCallLog({})\n```\n **/ \ndeclare function clearCallLog(): { cleared: boolean; };\n \n/**\n * Updates the response map for the current task at runtime.\n *\n * \n * @example usage\n```javascript\nawait connectors.testing.setResponses({\n responses: { \"someConnector.create\": { id: \"456\" } }\n})\n```\n **/ \ndeclare function setResponses(): { updated: boolean; responseCount: number; };\n ",
3
+ "methods": [
4
+ "getCallLog",
5
+ "clearCallLog",
6
+ "setResponses"
7
+ ]
8
+ }
@@ -0,0 +1,6 @@
1
+ // stands in for AbstractController from @aloma.io/integration-sdk
2
+ export class AbstractController {
3
+ protected config: any = {};
4
+ async inheritedPublic(): Promise<void> {}
5
+ }
6
+ export {};
@@ -0,0 +1,204 @@
1
+ import { AbstractController } from "./abstract.mjs";
2
+
3
+ interface CallRecord {
4
+ method: string;
5
+ args: any;
6
+ timestamp: string;
7
+ }
8
+
9
+ /**
10
+ * Per-task scoped mock connector for ALOMA test workflows.
11
+ *
12
+ * State (responseMap, callLog) is keyed by ctx.taskId so concurrent
13
+ * test tasks against the same Testing pod do not contaminate each
14
+ * other's stubbed responses or recorded calls.
15
+ *
16
+ * Requires @aloma.io/integration-sdk ^3.8.67, which forwards
17
+ * { taskId, namespace, sticky } as the second arg to controller
18
+ * method handlers and to the __default fallback.
19
+ *
20
+ * Backward compatibility: when ctx is missing (legacy invocation or
21
+ * non-task call from admin UI), state falls into the "__no_ctx__"
22
+ * bucket so the connector does not crash. Isolation is NOT guaranteed
23
+ * in that mode — it is degraded compatibility only.
24
+ */
25
+ export default class Controller extends AbstractController {
26
+ // Per-task scoped state — Maps keyed by taskId (insertion order = LRU order)
27
+ private responseMapByTask: Map<string, Record<string, any>> = new Map();
28
+ private callLogByTask: Map<string, CallRecord[]> = new Map();
29
+ private recording: boolean = true;
30
+ private defaultResponses: Record<string, any> = {};
31
+ // Memory cap (LRU). Configurable via config.maxTasks; default 1000.
32
+ private maxTasks: number = 1000;
33
+
34
+ protected async start() {
35
+ this.loadConfig();
36
+ }
37
+
38
+ private loadConfig() {
39
+ const config = this.config;
40
+
41
+ // Parse default responses from connector config — these seed each
42
+ // task's bucket when first touched.
43
+ if (config.responses) {
44
+ try {
45
+ this.defaultResponses =
46
+ typeof config.responses === "string"
47
+ ? JSON.parse(config.responses)
48
+ : config.responses;
49
+ } catch (e) {
50
+ this.defaultResponses = {};
51
+ }
52
+ } else {
53
+ this.defaultResponses = {};
54
+ }
55
+
56
+ this.recording = config.recordCalls !== "false";
57
+
58
+ // Allow config-driven override of LRU cap. Default 1000 if not set
59
+ // or not a positive number.
60
+ const configured = Number(config.maxTasks);
61
+ if (Number.isFinite(configured) && configured > 0) {
62
+ this.maxTasks = Math.floor(configured);
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Resolve the bucket key for the current invocation. When ctx is
68
+ * absent or lacks taskId, falls back to a single shared bucket so
69
+ * legacy / hand-rolled callers do not crash. Isolation is not
70
+ * guaranteed in that mode.
71
+ */
72
+ private bucketKey(ctx?: { taskId?: string }): string {
73
+ return ctx && ctx.taskId ? ctx.taskId : "__no_ctx__";
74
+ }
75
+
76
+ /**
77
+ * LRU touch — ensures the bucket exists for the given key, evicting
78
+ * the least-recently-used entry if the cap would be exceeded. Map
79
+ * iteration order is insertion order; reinserting promotes a key to
80
+ * "most recently used" position.
81
+ */
82
+ private touchTask(key: string) {
83
+ // If not present and at cap, evict the oldest (insertion order)
84
+ if (!this.responseMapByTask.has(key)) {
85
+ if (this.responseMapByTask.size >= this.maxTasks) {
86
+ const firstKey = this.responseMapByTask.keys().next().value as
87
+ | string
88
+ | undefined;
89
+ if (firstKey !== undefined) {
90
+ this.responseMapByTask.delete(firstKey);
91
+ this.callLogByTask.delete(firstKey);
92
+ }
93
+ }
94
+ this.responseMapByTask.set(key, { ...this.defaultResponses });
95
+ this.callLogByTask.set(key, []);
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Catch-all handler — registered as __default by the SDK via the
101
+ * fallback override below. Looks up the response in the task-scoped
102
+ * map, records the call in the task-scoped log, and returns the
103
+ * configured response or a default stub acknowledgement.
104
+ */
105
+ private async handleStubCall(
106
+ arg: any,
107
+ ctx?: { taskId?: string },
108
+ ): Promise<any> {
109
+ const key = this.bucketKey(ctx);
110
+ this.touchTask(key);
111
+
112
+ const method = Array.isArray(arg?.__method)
113
+ ? arg.__method.join(".")
114
+ : arg?.__method || "unknown";
115
+
116
+ if (this.recording) {
117
+ const log = this.callLogByTask.get(key)!;
118
+ log.push({
119
+ method,
120
+ args: { ...arg, __method: undefined },
121
+ timestamp: new Date().toISOString(),
122
+ });
123
+ }
124
+
125
+ const responseMap = this.responseMapByTask.get(key)!;
126
+ if (responseMap[method] !== undefined) {
127
+ return responseMap[method];
128
+ }
129
+ if (responseMap["default"] !== undefined) {
130
+ return responseMap["default"];
131
+ }
132
+
133
+ return { stubbed: true, method };
134
+ }
135
+
136
+ /**
137
+ * SDK override hook — invoked by AbstractController.__default with
138
+ * the per-invocation ctx (taskId, namespace, sticky) as second arg.
139
+ */
140
+ protected fallback(arg: any, ctx?: { taskId?: string }): Promise<any> {
141
+ return this.handleStubCall(arg, ctx);
142
+ }
143
+
144
+ /**
145
+ * Returns recorded calls scoped to the current task.
146
+ * @example
147
+ * ```javascript
148
+ * const log = await connectors.testing.getCallLog({})
149
+ * // log.calls = [{method, args, timestamp}, ...] for THIS task only
150
+ * ```
151
+ */
152
+ async getCallLog(_args: any, ctx?: { taskId?: string }) {
153
+ const key = this.bucketKey(ctx);
154
+ const calls = this.callLogByTask.get(key) || [];
155
+ return { calls: [...calls], count: calls.length };
156
+ }
157
+
158
+ /**
159
+ * Resets the call log for the current task only.
160
+ * @example
161
+ * ```javascript
162
+ * await connectors.testing.clearCallLog({})
163
+ * ```
164
+ */
165
+ async clearCallLog(_args: any, ctx?: { taskId?: string }) {
166
+ const key = this.bucketKey(ctx);
167
+ if (this.callLogByTask.has(key)) {
168
+ this.callLogByTask.set(key, []);
169
+ }
170
+ return { cleared: true };
171
+ }
172
+
173
+ /**
174
+ * Updates the response map for the current task at runtime.
175
+ * @example
176
+ * ```javascript
177
+ * await connectors.testing.setResponses({
178
+ * responses: { "someConnector.create": { id: "456" } }
179
+ * })
180
+ * ```
181
+ */
182
+ async setResponses(args: any, ctx?: { taskId?: string }) {
183
+ const key = this.bucketKey(ctx);
184
+ this.touchTask(key);
185
+
186
+ if (args?.responses) {
187
+ try {
188
+ const newResponses =
189
+ typeof args.responses === "string"
190
+ ? JSON.parse(args.responses)
191
+ : args.responses;
192
+ const existing = this.responseMapByTask.get(key)!;
193
+ this.responseMapByTask.set(key, { ...existing, ...newResponses });
194
+ } catch (e) {
195
+ // ignore parse errors — leave existing map untouched
196
+ }
197
+ }
198
+
199
+ return {
200
+ updated: true,
201
+ responseCount: Object.keys(this.responseMapByTask.get(key)!).length,
202
+ };
203
+ }
204
+ }
@@ -0,0 +1 @@
1
+ {"compilerOptions":{"target":"es2022","module":"node16","moduleResolution":"node16","skipLibCheck":true,"strict":true,"noImplicitAny":false,"experimentalDecorators":true,"esModuleInterop":true},"include":["src/**/*"]}
@@ -0,0 +1,5 @@
1
+ // stands in for AbstractController from @aloma.io/integration-sdk
2
+ export class AbstractController {
3
+ protected config: any = {};
4
+ async inheritedPublic(): Promise<void> {}
5
+ }
@@ -0,0 +1,9 @@
1
+ import {AbstractController} from './abstract.mjs';
2
+
3
+ export interface Extra {
4
+ a: string;
5
+ }
6
+
7
+ export default class Controller extends AbstractController {
8
+ async one() {}
9
+ }
@@ -0,0 +1 @@
1
+ {"compilerOptions":{"target":"es2022","module":"node16","moduleResolution":"node16","skipLibCheck":true,"strict":true,"noImplicitAny":false,"experimentalDecorators":true,"esModuleInterop":true},"include":["src/**/*"]}
@@ -0,0 +1,5 @@
1
+ // stands in for AbstractController from @aloma.io/integration-sdk
2
+ export class AbstractController {
3
+ protected config: any = {};
4
+ async inheritedPublic(): Promise<void> {}
5
+ }
@@ -0,0 +1,7 @@
1
+ import {AbstractController} from './abstract.mjs';
2
+
3
+ export default class Controller extends AbstractController {
4
+ async one(): Promise<number> {
5
+ return 'not a number';
6
+ }
7
+ }
@@ -0,0 +1 @@
1
+ {"compilerOptions":{"target":"es2022","module":"node16","moduleResolution":"node16","skipLibCheck":true,"strict":true,"noImplicitAny":false,"experimentalDecorators":true,"esModuleInterop":true},"include":["src/**/*"]}
@@ -0,0 +1,8 @@
1
+ {
2
+ "text": "\n/**\n * \n *\n * \n * \n **/ \ndeclare function typed({email; count; tags; mode; either; nested}: {\n/**\n\n */\n email: string; \n/**\n\n */\n count: any; \n/**\n\n */\n tags: string[]; \n/**\n\n */\n mode: any; \n/**\n\n */\n either: any; \n/**\n\n */\n nested: any}): { ok: boolean; tags: number[]; };\n \n/**\n * \n *\n * \n * \n **/ \ndeclare function tuple(): [string, number];\n \n/**\n * \n *\n * \n * \n **/ \ndeclare function maybe(): string | undefined;\n ",
3
+ "methods": [
4
+ "typed",
5
+ "tuple",
6
+ "maybe"
7
+ ]
8
+ }
@@ -0,0 +1,5 @@
1
+ // stands in for AbstractController from @aloma.io/integration-sdk
2
+ export class AbstractController {
3
+ protected config: any = {};
4
+ async inheritedPublic(): Promise<void> {}
5
+ }
@@ -0,0 +1,13 @@
1
+ import {AbstractController} from './abstract.mjs';
2
+
3
+ export default class Controller extends AbstractController {
4
+ async typed({email, count, tags, mode, either, nested}: {email: string; count?: number; tags: string[]; mode: 'a' | 'b'; either: string | number; nested: {x: number}}) {
5
+ return {ok: true, tags: [1, 2]};
6
+ }
7
+ async tuple(): Promise<[string, number]> {
8
+ return ['a', 1];
9
+ }
10
+ async maybe(): Promise<string | undefined> {
11
+ return undefined;
12
+ }
13
+ }
@@ -0,0 +1 @@
1
+ {"compilerOptions":{"target":"es2022","module":"node16","moduleResolution":"node16","skipLibCheck":true,"strict":true,"noImplicitAny":false,"experimentalDecorators":true,"esModuleInterop":true},"include":["src/**/*"]}
@@ -0,0 +1,9 @@
1
+ {
2
+ "text": "\n/**\n * \n *\n * \n * \n **/ \ndeclare function visible({a}: {\n/**\n\n */\n a: string}): string;\n \n/**\n * \n *\n * \n * \n **/ \ndeclare function quoted-name({a}: {\n/**\n\n */\n a: string}): void;\n \n/**\n * \n *\n * \n * \n **/ \ndeclare function arrow({a}: {\n/**\n\n */\n a: string}): string;\n \n/**\n * \n *\n * \n * \n **/ \ndeclare function inheritedPublic(): void;\n ",
3
+ "methods": [
4
+ "visible",
5
+ "quoted-name",
6
+ "arrow",
7
+ "inheritedPublic"
8
+ ]
9
+ }
@@ -0,0 +1,5 @@
1
+ // stands in for AbstractController from @aloma.io/integration-sdk
2
+ export class AbstractController {
3
+ protected config: any = {};
4
+ async inheritedPublic(): Promise<void> {}
5
+ }
@@ -0,0 +1,19 @@
1
+ import {AbstractController} from './abstract.mjs';
2
+
3
+ export default class Controller extends AbstractController {
4
+ async visible({a}: {a: string}) {
5
+ return a;
6
+ }
7
+ private async hidden({a}: {a: string}) {}
8
+ protected async guarded({a}: {a: string}) {}
9
+ static async stat({a}: {a: string}) {}
10
+ async _underscore({a}: {a: string}) {}
11
+ async 'quoted-name'({a}: {a: string}) {}
12
+ async '_quoted_private'({a}: {a: string}) {}
13
+ arrow = async ({a}: {a: string}) => a;
14
+ notAMethod = 5;
15
+ get getter() {
16
+ return 1;
17
+ }
18
+ override async inheritedPublic() {}
19
+ }
@@ -0,0 +1 @@
1
+ {"compilerOptions":{"target":"es2022","module":"node16","moduleResolution":"node16","skipLibCheck":true,"strict":true,"noImplicitAny":false,"experimentalDecorators":true,"esModuleInterop":true},"include":["src/**/*"]}
@@ -0,0 +1,68 @@
1
+ // The transform reads a connector's controller source and returns the method declarations (`text`) and names
2
+ // (`methods`) that `cli build` writes to build/.controller.json. It replaced @ts-ast-parser/core (engines stop at
3
+ // Node 20), so most cases below are PARITY cases: expected.json was produced by the previous implementation (the
4
+ // ts-ast-parser one, SDK 3.8.68) on the same source, and the new one must print exactly the same. The same check
5
+ // ran on 58 real connector controllers (all of aloma-io/connector-*) with no difference.
6
+ // Needs `yarn build` first. Offline.
7
+ import assert from 'node:assert/strict';
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import {fileURLToPath} from 'node:url';
11
+ import {describe, it} from 'mocha';
12
+ import transform from '../build/transform/index.mjs';
13
+
14
+ const fixtures = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', 'transform');
15
+
16
+ // the transform reads tsconfig.json from the working directory, as `cli build` runs it from the connector folder
17
+ const run = async (name) => {
18
+ const before = process.cwd();
19
+ process.chdir(path.join(fixtures, name));
20
+ try {
21
+ return await transform('./src/controller/index.mts');
22
+ } finally {
23
+ process.chdir(before);
24
+ }
25
+ };
26
+
27
+ const expected = (name) => JSON.parse(fs.readFileSync(path.join(fixtures, name, 'expected.json'), 'utf8'));
28
+
29
+ describe('transform (controller source to declarations)', function () {
30
+ this.timeout(60000);
31
+
32
+ for (const [name, what] of [
33
+ ['basic', 'JSDoc description, @param, @example, @namespace, defaults, return types'],
34
+ ['visibility', 'public, private, protected, static, underscore, quoted names, arrow property, getter, inherited'],
35
+ ['typed', 'typed object parameters, unions, arrays, tuples, optional'],
36
+ ['overloads', 'one declaration per overload signature'],
37
+ ['tdd-stub', 'the real connector-tdd-stub controller'],
38
+ ]) {
39
+ it(`prints exactly what the previous implementation printed: ${name} (${what})`, async () => {
40
+ assert.deepEqual(await run(name), expected(name));
41
+ });
42
+ }
43
+
44
+ it('lists the public methods the class declares or overrides, not inherited, private, static or underscore ones', async () => {
45
+ const {methods} = await run('visibility');
46
+ assert.deepEqual(methods, ['visible', 'quoted-name', 'arrow', 'inheritedPublic']);
47
+ });
48
+
49
+ it('lists the properties of a parameter typed with an interface (the previous implementation crashed)', async () => {
50
+ const {text, methods} = await run('interface-param');
51
+ assert.deepEqual(methods, ['viaInterface']);
52
+ assert.match(text, /declare function viaInterface\(\{id; n\}: \{/);
53
+ assert.match(text, /\n id: string; /);
54
+ assert.match(text, /\n n: number \| undefined\}\): \{ id: string; n: number \| undefined; \};/);
55
+ });
56
+
57
+ it('fails when the file exports more than the default class', async () => {
58
+ await assert.rejects(run('two-exports'), /connector file needs to export default class/);
59
+ });
60
+
61
+ it('fails with the TypeScript error text when the source does not compile (the previous one died on a circular JSON error)', async () => {
62
+ await assert.rejects(run('type-error'), (err) => {
63
+ assert.match(err.message, /has TypeScript errors/);
64
+ assert.match(err.message, /index\.mts\(5,5\).*TS2322.*'string' is not assignable to type 'number'/);
65
+ return true;
66
+ });
67
+ });
68
+ });