@relaybus/eslint-plugin 0.0.0-stage → 0.3.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Berkcan Şavur
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,93 @@
1
- # Temporary Holding Version
1
+ # @relaybus/eslint-plugin
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ @relaybus/eslint-plugin is the ESLint plugin of relaybus, a reliable, storage-agnostic event bus for NestJS and Node.js: it keeps outbox transactions short and events atomic with your writes by flagging calls to other services inside a transaction and `publish` calls outside one.
4
+
5
+ ```sh
6
+ pnpm add -D @relaybus/eslint-plugin
7
+ ```
8
+
9
+ ESLint 9 or newer, flat config. For TypeScript files with decorators use the `typescript-eslint` parser (type information is not needed).
10
+
11
+ ```js
12
+ // eslint.config.js
13
+ import tseslint from 'typescript-eslint';
14
+ import relaybus from '@relaybus/eslint-plugin';
15
+
16
+ export default tseslint.config(...tseslint.configs.recommended, relaybus.configs.recommended);
17
+ ```
18
+
19
+ `recommended` turns both rules on at `error` under the plugin name `@relaybus`.
20
+
21
+ ## Where a transaction is
22
+
23
+ Both rules agree on what runs inside a transaction:
24
+
25
+ - a method decorated with `@RelayBusTransaction()`;
26
+ - a handler that runs in a transaction: `@RelayBusConsumer(Event, { transactional: true })`, an
27
+ `on<Event>` method of a `@RelayBusCatalogConsumer(Catalog, { transactional: true })` class (also
28
+ under the deprecated names `@Subscribe` and `@EventConsumer`), and
29
+ `bus.subscribe(Event, handler, { transactional: true })`;
30
+ - the callback of `x.transaction(...)`, `x.$transaction(...)` or `x.withTransaction(...)`
31
+ (`bus.transaction`, TypeORM, Knex, Sequelize, Prisma, MongoDB sessions);
32
+ - any method of the same class called through `this.method()` from one of the above, transitively.
33
+
34
+ ## `@relaybus/no-external-call-in-transaction`
35
+
36
+ Reports calls to other services inside a transaction: `fetch`, `axios` (`axios(...)`,
37
+ `axios.get(...)`, any method), `got`, `http.request` / `http.get` / `https.*` of Node.js, `undici`
38
+ (`request`, `fetch`, `stream`, `pipeline`), and `HttpService` of `@nestjs/axios`
39
+ (`this.httpService.get(...)`, `this.http.post(...)`, `httpService.axiosRef.get(...)`). Imports,
40
+ renamed imports and `require` are followed.
41
+
42
+ A transaction holds its row locks and a database connection until it ends. An HTTP call inside it
43
+ makes every other writer of those rows wait for the remote service, and relaybus rolls the
44
+ transaction back with `TransactionTimeoutError` once it outlives `transactionTimeout`.
45
+
46
+ ```ts
47
+ // Reported
48
+ @RelayBusTransaction()
49
+ async place(order: Order): Promise<void> {
50
+ const price = await this.httpService.axiosRef.get(`/prices/${order.sku}`);
51
+ await this.orders.insert({ ...order, price: price.data });
52
+ await this.bus.publish(new OrderPlaced({ orderId: order.id }));
53
+ }
54
+
55
+ // Fixed: call before the transaction, and react to the event after the commit
56
+ async place(order: Order): Promise<void> {
57
+ const price = await this.httpService.axiosRef.get(`/prices/${order.sku}`);
58
+ await this.save({ ...order, price: price.data });
59
+ }
60
+
61
+ @RelayBusTransaction()
62
+ private async save(order: PricedOrder): Promise<void> {
63
+ await this.orders.insert(order);
64
+ await this.bus.publish(new OrderPlaced({ orderId: order.id }));
65
+ }
66
+ ```
67
+
68
+ ## `@relaybus/publish-requires-transaction`
69
+
70
+ Reports `publish` / `publishAll` on a relaybus bus or catalog publisher outside a transaction
71
+ without `{ transaction }`. Such a publish is not atomic with your writes and throws
72
+ `TransactionRequiredError` at runtime (`transactionMode: 'required'`, the default). Options that
73
+ the rule cannot see into (a variable, a spread) are trusted.
74
+
75
+ The publishers are recognised by name: the object's last name ends in `bus`, `Bus`, `publisher` or
76
+ `Publisher` (`this.bus`, `eventBus`, `orderPublisher`). Set your own pattern when you name them
77
+ differently:
78
+
79
+ ```js
80
+ { rules: { '@relaybus/publish-requires-transaction': ['error', { publishers: '^(bus|orders)$' }] } }
81
+ ```
82
+
83
+ ## Limits
84
+
85
+ The rules read one file and know names, not types: a call to a service hidden behind your own
86
+ wrapper function, or a transaction opened in another file, is not seen. The runtime checks stay in
87
+ place behind them (`TransactionRequiredError`, `transactionTimeout`).
88
+
89
+ Documentation: https://berkcansavur.github.io/relaybus/recipes/transactions-in-nestjs
90
+
91
+ ## License
92
+
93
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,342 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ // src/ast.ts
6
+ var FUNCTION_TYPES = /* @__PURE__ */ new Set([
7
+ "FunctionExpression",
8
+ "ArrowFunctionExpression",
9
+ "FunctionDeclaration"
10
+ ]);
11
+ function isFunction(node) {
12
+ return FUNCTION_TYPES.has(node.type);
13
+ }
14
+ function findDecorator(node, names) {
15
+ for (const { expression } of node.decorators ?? []) {
16
+ if (expression.type === "CallExpression" && expression.callee.type === "Identifier" && names.has(expression.callee.name)) {
17
+ return expression;
18
+ }
19
+ }
20
+ return void 0;
21
+ }
22
+ function hasTransactionalOption(call) {
23
+ return call.arguments.some(
24
+ (argument) => argument.type === "ObjectExpression" && argument.properties.some(
25
+ (property) => property.type === "Property" && definitionName(property) === "transactional" && property.value.type === "Literal" && property.value.value === true
26
+ )
27
+ );
28
+ }
29
+ function keyName(key) {
30
+ if (key.type === "Identifier") {
31
+ return key.name;
32
+ }
33
+ if (key.type === "PrivateIdentifier") {
34
+ return `#${key.name}`;
35
+ }
36
+ return key.type === "Literal" && typeof key.value === "string" ? key.value : void 0;
37
+ }
38
+ function memberName(member) {
39
+ return staticName(member.computed, member.property);
40
+ }
41
+ function definitionName(definition) {
42
+ return staticName(definition.computed, definition.key);
43
+ }
44
+ function staticName(computed, key) {
45
+ return computed && key.type !== "Literal" ? void 0 : keyName(key);
46
+ }
47
+ function lastName(node) {
48
+ if (node.type === "Identifier") {
49
+ return node.name;
50
+ }
51
+ return node.type === "MemberExpression" ? memberName(node) : void 0;
52
+ }
53
+
54
+ // src/external-calls.ts
55
+ var MODULES = {
56
+ axios: "axios",
57
+ got: "got",
58
+ http: "http",
59
+ "node:http": "http",
60
+ https: "http",
61
+ "node:https": "http",
62
+ undici: "undici"
63
+ };
64
+ var REQUEST_MEMBERS = {
65
+ axios: void 0,
66
+ got: void 0,
67
+ http: /* @__PURE__ */ new Set(["request", "get"]),
68
+ undici: /* @__PURE__ */ new Set(["request", "fetch", "stream", "pipeline"])
69
+ };
70
+ var HTTP_SERVICE_NAME = /^(?:http|httpService|axiosRef)$/i;
71
+ var ExternalCallDetector = class {
72
+ /** Local names of imported HTTP client modules. */
73
+ #modules = /* @__PURE__ */ new Map([
74
+ ["axios", "axios"],
75
+ ["got", "got"]
76
+ ]);
77
+ /** Local names of imported request functions, such as `request` from `undici`. */
78
+ #functions = /* @__PURE__ */ new Set(["fetch"]);
79
+ /** Records `import x from 'axios'`, `import * as https from 'node:https'`, `import { request } from 'undici'`. */
80
+ recordImport(declaration) {
81
+ const module = MODULES[String(declaration.source.value)];
82
+ if (module === void 0) {
83
+ return;
84
+ }
85
+ for (const specifier of declaration.specifiers) {
86
+ if (specifier.type !== "ImportSpecifier") {
87
+ this.#modules.set(specifier.local.name, module);
88
+ } else if (REQUEST_MEMBERS[module]?.has(importedName(specifier)) === true) {
89
+ this.#functions.add(specifier.local.name);
90
+ }
91
+ }
92
+ }
93
+ /** Records `const axios = require('axios')`. */
94
+ recordRequire(declarator) {
95
+ const { id, init } = declarator;
96
+ if (id.type === "Identifier" && init?.type === "CallExpression" && lastName(init.callee) === "require" && init.arguments[0]?.type === "Literal") {
97
+ const module = MODULES[String(init.arguments[0].value)];
98
+ if (module !== void 0) {
99
+ this.#modules.set(id.name, module);
100
+ }
101
+ }
102
+ }
103
+ /** Whether `call` sends a request to another service. */
104
+ isExternalCall(call) {
105
+ const { callee } = call;
106
+ if (callee.type === "Identifier") {
107
+ const module = this.#modules.get(callee.name);
108
+ return this.#functions.has(callee.name) || module === "axios" || module === "got";
109
+ }
110
+ return callee.type === "MemberExpression" && this.#isClientMember(callee);
111
+ }
112
+ /** `axios.post`, `https.request`, `this.httpService.get`, `httpService.axiosRef.get`. */
113
+ #isClientMember(callee) {
114
+ if (HTTP_SERVICE_NAME.test(lastName(callee.object) ?? "")) {
115
+ return true;
116
+ }
117
+ const module = callee.object.type === "Identifier" ? this.#modules.get(callee.object.name) : void 0;
118
+ if (module === void 0) {
119
+ return false;
120
+ }
121
+ return REQUEST_MEMBERS[module]?.has(memberName(callee) ?? "") ?? true;
122
+ }
123
+ };
124
+ function importedName(specifier) {
125
+ const { imported } = specifier;
126
+ return imported.type === "Identifier" ? imported.name : String(imported.value);
127
+ }
128
+
129
+ // src/transaction-scopes.ts
130
+ var TRANSACTION_METHODS = /* @__PURE__ */ new Set(["transaction", "$transaction", "withTransaction"]);
131
+ var TRANSACTION_DECORATORS = /* @__PURE__ */ new Set(["RelayBusTransaction"]);
132
+ var CONSUMER_DECORATORS = /* @__PURE__ */ new Set(["RelayBusConsumer", "Subscribe"]);
133
+ var CATALOG_CONSUMER_DECORATORS = /* @__PURE__ */ new Set(["RelayBusCatalogConsumer", "EventConsumer"]);
134
+ var CATALOG_HANDLER_NAME = /^on[A-Z]/;
135
+ function isTransactionalMethod(method) {
136
+ if (findDecorator(method, TRANSACTION_DECORATORS) !== void 0) {
137
+ return true;
138
+ }
139
+ const consumer = findDecorator(method, CONSUMER_DECORATORS);
140
+ if (consumer !== void 0 && hasTransactionalOption(consumer)) {
141
+ return true;
142
+ }
143
+ const catalogConsumer = findDecorator(method.parent.parent, CATALOG_CONSUMER_DECORATORS);
144
+ return catalogConsumer !== void 0 && hasTransactionalOption(catalogConsumer) && CATALOG_HANDLER_NAME.test(definitionName(method) ?? "");
145
+ }
146
+ function isTransactionScope(fn) {
147
+ const parent = fn.parent;
148
+ if (parent.type === "MethodDefinition") {
149
+ return isTransactionalMethod(parent);
150
+ }
151
+ if (parent.type !== "CallExpression" || parent.callee.type !== "MemberExpression") {
152
+ return false;
153
+ }
154
+ const method = memberName(parent.callee);
155
+ if (TRANSACTION_METHODS.has(method ?? "")) {
156
+ return parent.arguments[0] === fn;
157
+ }
158
+ return method === "subscribe" && parent.arguments[1] === fn && hasTransactionalOption(parent);
159
+ }
160
+
161
+ // src/transaction-tracker.ts
162
+ var TransactionTracker = class {
163
+ #sourceCode;
164
+ #thisCalls = [];
165
+ #transactional = /* @__PURE__ */ new Map();
166
+ constructor(sourceCode) {
167
+ this.#sourceCode = sourceCode;
168
+ }
169
+ record(call) {
170
+ const { callee } = call;
171
+ if (callee.type !== "MemberExpression" || callee.object.type !== "ThisExpression") {
172
+ return;
173
+ }
174
+ const method = memberName(callee);
175
+ const classBody = this.#ancestors(call).findLast((ancestor) => ancestor.type === "ClassBody");
176
+ if (method !== void 0 && classBody !== void 0) {
177
+ this.#thisCalls.push({ node: call, method, classBody });
178
+ }
179
+ }
180
+ isInTransaction(node) {
181
+ return this.#isInTransaction(node, (classBody) => this.#transactionalMethods(classBody));
182
+ }
183
+ #isInTransaction(node, methodsOf) {
184
+ for (const ancestor of this.#ancestors(node).reverse()) {
185
+ if (isFunction(ancestor) && isTransactionScope(ancestor)) {
186
+ return true;
187
+ }
188
+ if (ancestor.type === "MethodDefinition") {
189
+ return methodsOf(ancestor.parent).has(definitionName(ancestor) ?? "");
190
+ }
191
+ }
192
+ return false;
193
+ }
194
+ #ancestors(node) {
195
+ return this.#sourceCode.getAncestors(node);
196
+ }
197
+ /** Methods of a class that run in a transaction, directly or through `this.method()` calls. */
198
+ #transactionalMethods(classBody) {
199
+ const known = this.#transactional.get(classBody);
200
+ if (known !== void 0) {
201
+ return known;
202
+ }
203
+ const methods = new Set(
204
+ classBody.body.filter((member) => member.type === "MethodDefinition").filter((member) => isTransactionalMethod(member)).map((member) => definitionName(member) ?? "")
205
+ );
206
+ const calls = this.#thisCalls.filter((call) => call.classBody === classBody);
207
+ for (let grew = true; grew; ) {
208
+ const added = calls.filter(
209
+ (call) => !methods.has(call.method) && this.#isInTransaction(call.node, () => methods)
210
+ );
211
+ added.forEach((call) => methods.add(call.method));
212
+ grew = added.length > 0;
213
+ }
214
+ this.#transactional.set(classBody, methods);
215
+ return methods;
216
+ }
217
+ };
218
+
219
+ // src/rules/no-external-call-in-transaction.ts
220
+ var noExternalCallInTransaction = {
221
+ meta: {
222
+ type: "problem",
223
+ docs: {
224
+ description: "Disallow calls to other services inside a database transaction",
225
+ url: "https://berkcansavur.github.io/relaybus/recipes/transactions-in-nestjs"
226
+ },
227
+ schema: [],
228
+ messages: {
229
+ externalCall: "{{call}} calls another service inside a transaction. The transaction keeps its row locks and its database connection until the call returns, and relaybus rolls it back with TransactionTimeoutError once it outlives transactionTimeout. Make the call before the transaction starts or after it commits (for example from the handler of an event you publish)."
230
+ }
231
+ },
232
+ create(context) {
233
+ const tracker = new TransactionTracker(context.sourceCode);
234
+ const detector = new ExternalCallDetector();
235
+ const calls = [];
236
+ return {
237
+ ImportDeclaration: (node) => detector.recordImport(node),
238
+ VariableDeclarator: (node) => detector.recordRequire(node),
239
+ CallExpression: (node) => {
240
+ tracker.record(node);
241
+ calls.push(node);
242
+ },
243
+ "Program:exit": () => {
244
+ for (const call of calls) {
245
+ if (detector.isExternalCall(call) && tracker.isInTransaction(call)) {
246
+ context.report({
247
+ node: call,
248
+ messageId: "externalCall",
249
+ data: { call: `${context.sourceCode.getText(call.callee)}()` }
250
+ });
251
+ }
252
+ }
253
+ }
254
+ };
255
+ }
256
+ };
257
+
258
+ // src/rules/publish-requires-transaction.ts
259
+ var PUBLISH_METHODS = /* @__PURE__ */ new Set(["publish", "publishAll"]);
260
+ var DEFAULT_PUBLISHERS = "(?:^|[a-z_$])(?:[Bb]us|[Pp]ublisher)$|^(?:bus|publisher)$";
261
+ function passesTransaction(call) {
262
+ const options = call.arguments[1];
263
+ if (options === void 0) {
264
+ return false;
265
+ }
266
+ if (options.type !== "ObjectExpression") {
267
+ return true;
268
+ }
269
+ return options.properties.some(
270
+ (property) => property.type === "SpreadElement" || definitionName(property) === "transaction"
271
+ );
272
+ }
273
+ var publishRequiresTransaction = {
274
+ meta: {
275
+ type: "problem",
276
+ docs: {
277
+ description: "Require relaybus publish calls to run inside a transaction",
278
+ url: "https://berkcansavur.github.io/relaybus/recipes/transactions-in-nestjs"
279
+ },
280
+ schema: [
281
+ {
282
+ type: "object",
283
+ properties: { publishers: { type: "string" } },
284
+ additionalProperties: false
285
+ }
286
+ ],
287
+ messages: {
288
+ noTransaction: "{{call}} runs outside a transaction, so the event is not atomic with your writes and relaybus throws TransactionRequiredError. Publish inside a @RelayBusTransaction() method or a bus.transaction(...) callback, or pass { transaction }."
289
+ }
290
+ },
291
+ create(context) {
292
+ const [options = {}] = context.options;
293
+ const publishers = new RegExp(options.publishers ?? DEFAULT_PUBLISHERS);
294
+ const tracker = new TransactionTracker(context.sourceCode);
295
+ const publishes = [];
296
+ return {
297
+ CallExpression: (node) => {
298
+ tracker.record(node);
299
+ const { callee } = node;
300
+ if (callee.type === "MemberExpression" && PUBLISH_METHODS.has(memberName(callee) ?? "") && publishers.test(lastName(callee.object) ?? "")) {
301
+ publishes.push(node);
302
+ }
303
+ },
304
+ "Program:exit": () => {
305
+ for (const call of publishes) {
306
+ if (!passesTransaction(call) && !tracker.isInTransaction(call)) {
307
+ context.report({
308
+ node: call,
309
+ messageId: "noTransaction",
310
+ data: { call: `${context.sourceCode.getText(call.callee)}()` }
311
+ });
312
+ }
313
+ }
314
+ }
315
+ };
316
+ }
317
+ };
318
+
319
+ // src/index.ts
320
+ var rules = {
321
+ "no-external-call-in-transaction": noExternalCallInTransaction,
322
+ "publish-requires-transaction": publishRequiresTransaction
323
+ };
324
+ var plugin = { meta: { name: "@relaybus/eslint-plugin" }, rules };
325
+ var configs = {
326
+ recommended: {
327
+ name: "@relaybus/recommended",
328
+ plugins: { "@relaybus": plugin },
329
+ rules: {
330
+ "@relaybus/no-external-call-in-transaction": "error",
331
+ "@relaybus/publish-requires-transaction": "error"
332
+ }
333
+ }
334
+ };
335
+ var relaybus = { ...plugin, configs };
336
+ var index_default = relaybus;
337
+
338
+ exports.configs = configs;
339
+ exports.default = index_default;
340
+ exports.rules = rules;
341
+ //# sourceMappingURL=index.cjs.map
342
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/ast.ts","../src/external-calls.ts","../src/transaction-scopes.ts","../src/transaction-tracker.ts","../src/rules/no-external-call-in-transaction.ts","../src/rules/publish-requires-transaction.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["import type { Rule } from 'eslint';\nimport type * as ESTree from 'estree';\n\n/** A function node as ESLint hands it to rules, with its parent. */\nexport type FunctionNode = Rule.Node &\n (ESTree.FunctionExpression | ESTree.ArrowFunctionExpression | ESTree.FunctionDeclaration);\n\n/** What the TypeScript parser adds to classes and class members. */\ntype Decorated = { readonly decorators?: readonly { readonly expression: ESTree.Node }[] };\n\nconst FUNCTION_TYPES = new Set([\n 'FunctionExpression',\n 'ArrowFunctionExpression',\n 'FunctionDeclaration',\n]);\n\nexport function isFunction(node: Rule.Node): node is FunctionNode {\n return FUNCTION_TYPES.has(node.type);\n}\n\n/** The call of the first decorator of `node` named one of `names`, such as `@RelayBusTransaction()`. */\nexport function findDecorator(\n node: object,\n names: ReadonlySet<string>,\n): ESTree.CallExpression | undefined {\n for (const { expression } of (node as Decorated).decorators ?? []) {\n if (\n expression.type === 'CallExpression' &&\n expression.callee.type === 'Identifier' &&\n names.has(expression.callee.name)\n ) {\n return expression;\n }\n }\n return undefined;\n}\n\n/** Whether an argument of `call` is an object literal with `transactional: true`. */\nexport function hasTransactionalOption(call: ESTree.CallExpression): boolean {\n return call.arguments.some(\n (argument) =>\n argument.type === 'ObjectExpression' &&\n argument.properties.some(\n (property) =>\n property.type === 'Property' &&\n definitionName(property) === 'transactional' &&\n property.value.type === 'Literal' &&\n property.value.value === true,\n ),\n );\n}\n\n/**\n * The name of a property key, class member or import: `name`, `#name` for a private member, the\n * text of a string literal (`'name'`); undefined for anything computed at runtime.\n */\nexport function keyName(key: ESTree.Node): string | undefined {\n if (key.type === 'Identifier') {\n return key.name;\n }\n if (key.type === 'PrivateIdentifier') {\n return `#${key.name}`;\n }\n return key.type === 'Literal' && typeof key.value === 'string' ? key.value : undefined;\n}\n\n/** The member a member expression reads: `get` for `a.get` and `a['get']`, undefined for `a[name]`. */\nexport function memberName(member: ESTree.MemberExpression): string | undefined {\n return staticName(member.computed, member.property);\n}\n\n/** The name of a class member or object property: `place` for `place()` and `['place']()`, undefined for `[name]()`. */\nexport function definitionName(\n definition: ESTree.MethodDefinition | ESTree.Property,\n): string | undefined {\n return staticName(definition.computed, definition.key);\n}\n\nfunction staticName(computed: boolean, key: ESTree.Node): string | undefined {\n return computed && key.type !== 'Literal' ? undefined : keyName(key);\n}\n\n/** The last name of an expression: `bus` for `bus`, `this.bus` and `app.services.bus`. */\nexport function lastName(node: ESTree.Node): string | undefined {\n if (node.type === 'Identifier') {\n return node.name;\n }\n return node.type === 'MemberExpression' ? memberName(node) : undefined;\n}\n","import type { Rule } from 'eslint';\nimport type * as ESTree from 'estree';\nimport { lastName, memberName } from './ast.js';\n\n/** HTTP client modules, by import source. */\ntype ClientModule = 'axios' | 'got' | 'http' | 'undici';\n\nconst MODULES: Readonly<Record<string, ClientModule>> = {\n axios: 'axios',\n got: 'got',\n http: 'http',\n 'node:http': 'http',\n https: 'http',\n 'node:https': 'http',\n undici: 'undici',\n};\n\n/** Members of a module object that send a request; `undefined`: every member does. */\nconst REQUEST_MEMBERS: Readonly<Record<ClientModule, ReadonlySet<string> | undefined>> = {\n axios: undefined,\n got: undefined,\n http: new Set(['request', 'get']),\n undici: new Set(['request', 'fetch', 'stream', 'pipeline']),\n};\n\n/** `HttpService` of `@nestjs/axios`, injected as `httpService` (or `http`), and its `axiosRef`. */\nconst HTTP_SERVICE_NAME = /^(?:http|httpService|axiosRef)$/i;\n\n/**\n * Recognises calls to other services: `fetch`, `axios` and `got` (called or through a method),\n * `http.request` / `https.get` of Node.js, `undici.request` and friends (imported or required),\n * and `HttpService` of `@nestjs/axios`.\n */\nexport class ExternalCallDetector {\n /** Local names of imported HTTP client modules. */\n readonly #modules = new Map<string, ClientModule>([\n ['axios', 'axios'],\n ['got', 'got'],\n ]);\n /** Local names of imported request functions, such as `request` from `undici`. */\n readonly #functions = new Set<string>(['fetch']);\n\n /** Records `import x from 'axios'`, `import * as https from 'node:https'`, `import { request } from 'undici'`. */\n recordImport(declaration: ESTree.ImportDeclaration): void {\n const module = MODULES[String(declaration.source.value)];\n if (module === undefined) {\n return;\n }\n for (const specifier of declaration.specifiers) {\n if (specifier.type !== 'ImportSpecifier') {\n this.#modules.set(specifier.local.name, module);\n } else if (REQUEST_MEMBERS[module]?.has(importedName(specifier)) === true) {\n this.#functions.add(specifier.local.name);\n }\n }\n }\n\n /** Records `const axios = require('axios')`. */\n recordRequire(declarator: ESTree.VariableDeclarator): void {\n const { id, init } = declarator;\n if (\n id.type === 'Identifier' &&\n init?.type === 'CallExpression' &&\n lastName(init.callee) === 'require' &&\n init.arguments[0]?.type === 'Literal'\n ) {\n const module = MODULES[String(init.arguments[0].value)];\n if (module !== undefined) {\n this.#modules.set(id.name, module);\n }\n }\n }\n\n /** Whether `call` sends a request to another service. */\n isExternalCall(call: Rule.Node & ESTree.CallExpression): boolean {\n const { callee } = call;\n if (callee.type === 'Identifier') {\n const module = this.#modules.get(callee.name);\n return this.#functions.has(callee.name) || module === 'axios' || module === 'got';\n }\n return callee.type === 'MemberExpression' && this.#isClientMember(callee);\n }\n\n /** `axios.post`, `https.request`, `this.httpService.get`, `httpService.axiosRef.get`. */\n #isClientMember(callee: ESTree.MemberExpression): boolean {\n if (HTTP_SERVICE_NAME.test(lastName(callee.object) ?? '')) {\n return true;\n }\n const module =\n callee.object.type === 'Identifier' ? this.#modules.get(callee.object.name) : undefined;\n if (module === undefined) {\n return false;\n }\n return REQUEST_MEMBERS[module]?.has(memberName(callee) ?? '') ?? true;\n }\n}\n\n/** `request` for `import { request }` and `import { 'request' as send }`. */\nfunction importedName(specifier: ESTree.ImportSpecifier): string {\n const { imported } = specifier;\n return imported.type === 'Identifier' ? imported.name : String(imported.value);\n}\n","import type { Rule } from 'eslint';\nimport type * as ESTree from 'estree';\nimport {\n findDecorator,\n definitionName,\n hasTransactionalOption,\n memberName,\n type FunctionNode,\n} from './ast.js';\n\n/** Methods whose callback runs in a database transaction: relaybus, TypeORM, Knex, Sequelize, Prisma, MongoDB sessions. */\nconst TRANSACTION_METHODS = new Set(['transaction', '$transaction', 'withTransaction']);\nconst TRANSACTION_DECORATORS = new Set(['RelayBusTransaction']);\n/** Method decorators of relaybus handlers, current name and deprecated alias. */\nconst CONSUMER_DECORATORS = new Set(['RelayBusConsumer', 'Subscribe']);\n/** Class decorators of relaybus catalog handlers, current name and deprecated alias. */\nconst CATALOG_CONSUMER_DECORATORS = new Set(['RelayBusCatalogConsumer', 'EventConsumer']);\nconst CATALOG_HANDLER_NAME = /^on[A-Z]/;\n\n/**\n * Whether `method` runs in a transaction by itself: `@RelayBusTransaction()`, a\n * `@RelayBusConsumer(..., { transactional: true })` handler, or an `on<Event>` method of a class\n * marked `@RelayBusCatalogConsumer(..., { transactional: true })` (or the deprecated names).\n */\nexport function isTransactionalMethod(method: Rule.Node & ESTree.MethodDefinition): boolean {\n if (findDecorator(method, TRANSACTION_DECORATORS) !== undefined) {\n return true;\n }\n const consumer = findDecorator(method, CONSUMER_DECORATORS);\n if (consumer !== undefined && hasTransactionalOption(consumer)) {\n return true;\n }\n // A method's parent is the class body, whose parent is the class.\n const catalogConsumer = findDecorator(method.parent.parent!, CATALOG_CONSUMER_DECORATORS);\n return (\n catalogConsumer !== undefined &&\n hasTransactionalOption(catalogConsumer) &&\n CATALOG_HANDLER_NAME.test(definitionName(method) ?? '')\n );\n}\n\n/**\n * Whether `fn` runs in a transaction: a transactional method, the callback of\n * `x.transaction(fn)` (`bus.transaction`, `dataSource.transaction`, `prisma.$transaction`,\n * `session.withTransaction`), or a handler subscribed with `{ transactional: true }`.\n */\nexport function isTransactionScope(fn: FunctionNode): boolean {\n const parent = fn.parent;\n if (parent.type === 'MethodDefinition') {\n return isTransactionalMethod(parent);\n }\n if (parent.type !== 'CallExpression' || parent.callee.type !== 'MemberExpression') {\n return false;\n }\n const method = memberName(parent.callee);\n if (TRANSACTION_METHODS.has(method ?? '')) {\n return parent.arguments[0] === fn;\n }\n return method === 'subscribe' && parent.arguments[1] === fn && hasTransactionalOption(parent);\n}\n","import type { Rule, SourceCode } from 'eslint';\nimport type * as ESTree from 'estree';\nimport { definitionName, isFunction, memberName } from './ast.js';\nimport { isTransactionScope, isTransactionalMethod } from './transaction-scopes.js';\n\n/** A `this.method()` call and the class body it is written in. */\ntype ThisCall = {\n readonly node: Rule.Node;\n readonly method: string;\n readonly classBody: Rule.Node;\n};\n\n/**\n * Tells whether a node runs inside a transaction. Besides transaction callbacks and transactional\n * methods, a method of the same class that is called with `this.method()` from inside a transaction\n * counts as well, transitively, so helpers of a `@RelayBusTransaction()` method are covered.\n * Record every call with {@link record} while traversing; ask {@link isInTransaction} at `Program:exit`.\n */\nexport class TransactionTracker {\n readonly #sourceCode: SourceCode;\n readonly #thisCalls: ThisCall[] = [];\n readonly #transactional = new Map<Rule.Node, Set<string>>();\n\n constructor(sourceCode: SourceCode) {\n this.#sourceCode = sourceCode;\n }\n\n record(call: Rule.Node & ESTree.CallExpression): void {\n const { callee } = call;\n if (callee.type !== 'MemberExpression' || callee.object.type !== 'ThisExpression') {\n return;\n }\n const method = memberName(callee);\n const classBody = this.#ancestors(call).findLast((ancestor) => ancestor.type === 'ClassBody');\n if (method !== undefined && classBody !== undefined) {\n this.#thisCalls.push({ node: call, method, classBody });\n }\n }\n\n isInTransaction(node: Rule.Node): boolean {\n return this.#isInTransaction(node, (classBody) => this.#transactionalMethods(classBody));\n }\n\n #isInTransaction(node: Rule.Node, methodsOf: (classBody: Rule.Node) => Set<string>): boolean {\n for (const ancestor of this.#ancestors(node).reverse()) {\n if (isFunction(ancestor) && isTransactionScope(ancestor)) {\n return true;\n }\n if (ancestor.type === 'MethodDefinition') {\n return methodsOf(ancestor.parent).has(definitionName(ancestor) ?? '');\n }\n }\n return false;\n }\n\n #ancestors(node: Rule.Node): Rule.Node[] {\n return this.#sourceCode.getAncestors(node) as Rule.Node[];\n }\n\n /** Methods of a class that run in a transaction, directly or through `this.method()` calls. */\n #transactionalMethods(classBody: Rule.Node): Set<string> {\n const known = this.#transactional.get(classBody);\n if (known !== undefined) {\n return known;\n }\n const methods = new Set(\n (classBody as Rule.Node & ESTree.ClassBody).body\n .filter((member) => member.type === 'MethodDefinition')\n .filter((member) => isTransactionalMethod(member as Rule.Node & ESTree.MethodDefinition))\n .map((member) => definitionName(member) ?? ''),\n );\n const calls = this.#thisCalls.filter((call) => call.classBody === classBody);\n for (let grew = true; grew;) {\n const added = calls.filter(\n (call) => !methods.has(call.method) && this.#isInTransaction(call.node, () => methods),\n );\n added.forEach((call) => methods.add(call.method));\n grew = added.length > 0;\n }\n this.#transactional.set(classBody, methods);\n return methods;\n }\n}\n","import type { Rule } from 'eslint';\nimport type * as ESTree from 'estree';\nimport { ExternalCallDetector } from '../external-calls.js';\nimport { TransactionTracker } from '../transaction-tracker.js';\n\n/**\n * Reports calls to other services (HTTP clients) made inside a transaction: a\n * `@RelayBusTransaction()` method, a transactional handler, a `bus.transaction` (or other\n * `.transaction`) callback, and the methods they call through `this`.\n */\nexport const noExternalCallInTransaction: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Disallow calls to other services inside a database transaction',\n url: 'https://berkcansavur.github.io/relaybus/recipes/transactions-in-nestjs',\n },\n schema: [],\n messages: {\n externalCall:\n '{{call}} calls another service inside a transaction. The transaction keeps its row locks ' +\n 'and its database connection until the call returns, and relaybus rolls it back with ' +\n 'TransactionTimeoutError once it outlives transactionTimeout. Make the call before the ' +\n 'transaction starts or after it commits (for example from the handler of an event you publish).',\n },\n },\n create(context) {\n const tracker = new TransactionTracker(context.sourceCode);\n const detector = new ExternalCallDetector();\n const calls: (Rule.Node & ESTree.CallExpression)[] = [];\n return {\n ImportDeclaration: (node) => detector.recordImport(node),\n VariableDeclarator: (node) => detector.recordRequire(node),\n CallExpression: (node) => {\n tracker.record(node);\n calls.push(node);\n },\n 'Program:exit': () => {\n for (const call of calls) {\n if (detector.isExternalCall(call) && tracker.isInTransaction(call)) {\n context.report({\n node: call,\n messageId: 'externalCall',\n data: { call: `${context.sourceCode.getText(call.callee)}()` },\n });\n }\n }\n },\n };\n },\n};\n","import type { Rule } from 'eslint';\nimport type * as ESTree from 'estree';\nimport { definitionName, lastName, memberName } from '../ast.js';\nimport { TransactionTracker } from '../transaction-tracker.js';\n\nconst PUBLISH_METHODS = new Set(['publish', 'publishAll']);\n/** Default names of the objects whose `publish` is a relaybus publish: `bus`, `this.eventBus`, `orderPublisher`. */\nconst DEFAULT_PUBLISHERS = '(?:^|[a-z_$])(?:[Bb]us|[Pp]ublisher)$|^(?:bus|publisher)$';\n\ntype Options = { readonly publishers?: string };\n\n/**\n * Whether `call` passes a transaction itself: `{ transaction }` in its options, or options the\n * rule cannot see into (a variable, a spread).\n */\nfunction passesTransaction(call: ESTree.CallExpression): boolean {\n const options = call.arguments[1];\n if (options === undefined) {\n return false;\n }\n if (options.type !== 'ObjectExpression') {\n return true;\n }\n return options.properties.some(\n (property) => property.type === 'SpreadElement' || definitionName(property) === 'transaction',\n );\n}\n\n/**\n * Reports `bus.publish` / `bus.publishAll` outside a transaction: not in a\n * `@RelayBusTransaction()` method, a transactional handler or a `.transaction` callback (or a\n * method they call through `this`), and without `{ transaction }`.\n */\nexport const publishRequiresTransaction: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Require relaybus publish calls to run inside a transaction',\n url: 'https://berkcansavur.github.io/relaybus/recipes/transactions-in-nestjs',\n },\n schema: [\n {\n type: 'object',\n properties: { publishers: { type: 'string' } },\n additionalProperties: false,\n },\n ],\n messages: {\n noTransaction:\n '{{call}} runs outside a transaction, so the event is not atomic with your writes and ' +\n 'relaybus throws TransactionRequiredError. Publish inside a @RelayBusTransaction() method ' +\n 'or a bus.transaction(...) callback, or pass { transaction }.',\n },\n },\n create(context) {\n const [options = {}] = context.options as [Options?];\n const publishers = new RegExp(options.publishers ?? DEFAULT_PUBLISHERS);\n const tracker = new TransactionTracker(context.sourceCode);\n const publishes: (Rule.Node & ESTree.CallExpression)[] = [];\n return {\n CallExpression: (node) => {\n tracker.record(node);\n const { callee } = node;\n if (\n callee.type === 'MemberExpression' &&\n PUBLISH_METHODS.has(memberName(callee) ?? '') &&\n publishers.test(lastName(callee.object) ?? '')\n ) {\n publishes.push(node);\n }\n },\n 'Program:exit': () => {\n for (const call of publishes) {\n if (!passesTransaction(call) && !tracker.isInTransaction(call)) {\n context.report({\n node: call,\n messageId: 'noTransaction',\n data: { call: `${context.sourceCode.getText(call.callee)}()` },\n });\n }\n }\n },\n };\n },\n};\n","import type { ESLint, Linter, Rule } from 'eslint';\nimport { noExternalCallInTransaction } from './rules/no-external-call-in-transaction.js';\nimport { publishRequiresTransaction } from './rules/publish-requires-transaction.js';\n\nexport const rules: Readonly<Record<string, Rule.RuleModule>> = {\n 'no-external-call-in-transaction': noExternalCallInTransaction,\n 'publish-requires-transaction': publishRequiresTransaction,\n};\n\nconst plugin = { meta: { name: '@relaybus/eslint-plugin' }, rules } satisfies ESLint.Plugin;\n\n/**\n * Both rules at error level, under the plugin name `@relaybus`:\n *\n * ```js\n * // eslint.config.js\n * import relaybus from '@relaybus/eslint-plugin';\n * export default [relaybus.configs.recommended];\n * ```\n */\nexport const configs: { readonly recommended: Linter.Config } = {\n recommended: {\n name: '@relaybus/recommended',\n plugins: { '@relaybus': plugin },\n rules: {\n '@relaybus/no-external-call-in-transaction': 'error',\n '@relaybus/publish-requires-transaction': 'error',\n },\n },\n};\n\nconst relaybus: ESLint.Plugin & { readonly configs: typeof configs } = { ...plugin, configs };\n\nexport default relaybus;\n"]}
@@ -0,0 +1,20 @@
1
+ import { Linter, ESLint, Rule } from 'eslint';
2
+
3
+ declare const rules: Readonly<Record<string, Rule.RuleModule>>;
4
+ /**
5
+ * Both rules at error level, under the plugin name `@relaybus`:
6
+ *
7
+ * ```js
8
+ * // eslint.config.js
9
+ * import relaybus from '@relaybus/eslint-plugin';
10
+ * export default [relaybus.configs.recommended];
11
+ * ```
12
+ */
13
+ declare const configs: {
14
+ readonly recommended: Linter.Config;
15
+ };
16
+ declare const relaybus: ESLint.Plugin & {
17
+ readonly configs: typeof configs;
18
+ };
19
+
20
+ export { configs, relaybus as default, rules };
@@ -0,0 +1,20 @@
1
+ import { Linter, ESLint, Rule } from 'eslint';
2
+
3
+ declare const rules: Readonly<Record<string, Rule.RuleModule>>;
4
+ /**
5
+ * Both rules at error level, under the plugin name `@relaybus`:
6
+ *
7
+ * ```js
8
+ * // eslint.config.js
9
+ * import relaybus from '@relaybus/eslint-plugin';
10
+ * export default [relaybus.configs.recommended];
11
+ * ```
12
+ */
13
+ declare const configs: {
14
+ readonly recommended: Linter.Config;
15
+ };
16
+ declare const relaybus: ESLint.Plugin & {
17
+ readonly configs: typeof configs;
18
+ };
19
+
20
+ export { configs, relaybus as default, rules };
package/dist/index.js ADDED
@@ -0,0 +1,336 @@
1
+ // src/ast.ts
2
+ var FUNCTION_TYPES = /* @__PURE__ */ new Set([
3
+ "FunctionExpression",
4
+ "ArrowFunctionExpression",
5
+ "FunctionDeclaration"
6
+ ]);
7
+ function isFunction(node) {
8
+ return FUNCTION_TYPES.has(node.type);
9
+ }
10
+ function findDecorator(node, names) {
11
+ for (const { expression } of node.decorators ?? []) {
12
+ if (expression.type === "CallExpression" && expression.callee.type === "Identifier" && names.has(expression.callee.name)) {
13
+ return expression;
14
+ }
15
+ }
16
+ return void 0;
17
+ }
18
+ function hasTransactionalOption(call) {
19
+ return call.arguments.some(
20
+ (argument) => argument.type === "ObjectExpression" && argument.properties.some(
21
+ (property) => property.type === "Property" && definitionName(property) === "transactional" && property.value.type === "Literal" && property.value.value === true
22
+ )
23
+ );
24
+ }
25
+ function keyName(key) {
26
+ if (key.type === "Identifier") {
27
+ return key.name;
28
+ }
29
+ if (key.type === "PrivateIdentifier") {
30
+ return `#${key.name}`;
31
+ }
32
+ return key.type === "Literal" && typeof key.value === "string" ? key.value : void 0;
33
+ }
34
+ function memberName(member) {
35
+ return staticName(member.computed, member.property);
36
+ }
37
+ function definitionName(definition) {
38
+ return staticName(definition.computed, definition.key);
39
+ }
40
+ function staticName(computed, key) {
41
+ return computed && key.type !== "Literal" ? void 0 : keyName(key);
42
+ }
43
+ function lastName(node) {
44
+ if (node.type === "Identifier") {
45
+ return node.name;
46
+ }
47
+ return node.type === "MemberExpression" ? memberName(node) : void 0;
48
+ }
49
+
50
+ // src/external-calls.ts
51
+ var MODULES = {
52
+ axios: "axios",
53
+ got: "got",
54
+ http: "http",
55
+ "node:http": "http",
56
+ https: "http",
57
+ "node:https": "http",
58
+ undici: "undici"
59
+ };
60
+ var REQUEST_MEMBERS = {
61
+ axios: void 0,
62
+ got: void 0,
63
+ http: /* @__PURE__ */ new Set(["request", "get"]),
64
+ undici: /* @__PURE__ */ new Set(["request", "fetch", "stream", "pipeline"])
65
+ };
66
+ var HTTP_SERVICE_NAME = /^(?:http|httpService|axiosRef)$/i;
67
+ var ExternalCallDetector = class {
68
+ /** Local names of imported HTTP client modules. */
69
+ #modules = /* @__PURE__ */ new Map([
70
+ ["axios", "axios"],
71
+ ["got", "got"]
72
+ ]);
73
+ /** Local names of imported request functions, such as `request` from `undici`. */
74
+ #functions = /* @__PURE__ */ new Set(["fetch"]);
75
+ /** Records `import x from 'axios'`, `import * as https from 'node:https'`, `import { request } from 'undici'`. */
76
+ recordImport(declaration) {
77
+ const module = MODULES[String(declaration.source.value)];
78
+ if (module === void 0) {
79
+ return;
80
+ }
81
+ for (const specifier of declaration.specifiers) {
82
+ if (specifier.type !== "ImportSpecifier") {
83
+ this.#modules.set(specifier.local.name, module);
84
+ } else if (REQUEST_MEMBERS[module]?.has(importedName(specifier)) === true) {
85
+ this.#functions.add(specifier.local.name);
86
+ }
87
+ }
88
+ }
89
+ /** Records `const axios = require('axios')`. */
90
+ recordRequire(declarator) {
91
+ const { id, init } = declarator;
92
+ if (id.type === "Identifier" && init?.type === "CallExpression" && lastName(init.callee) === "require" && init.arguments[0]?.type === "Literal") {
93
+ const module = MODULES[String(init.arguments[0].value)];
94
+ if (module !== void 0) {
95
+ this.#modules.set(id.name, module);
96
+ }
97
+ }
98
+ }
99
+ /** Whether `call` sends a request to another service. */
100
+ isExternalCall(call) {
101
+ const { callee } = call;
102
+ if (callee.type === "Identifier") {
103
+ const module = this.#modules.get(callee.name);
104
+ return this.#functions.has(callee.name) || module === "axios" || module === "got";
105
+ }
106
+ return callee.type === "MemberExpression" && this.#isClientMember(callee);
107
+ }
108
+ /** `axios.post`, `https.request`, `this.httpService.get`, `httpService.axiosRef.get`. */
109
+ #isClientMember(callee) {
110
+ if (HTTP_SERVICE_NAME.test(lastName(callee.object) ?? "")) {
111
+ return true;
112
+ }
113
+ const module = callee.object.type === "Identifier" ? this.#modules.get(callee.object.name) : void 0;
114
+ if (module === void 0) {
115
+ return false;
116
+ }
117
+ return REQUEST_MEMBERS[module]?.has(memberName(callee) ?? "") ?? true;
118
+ }
119
+ };
120
+ function importedName(specifier) {
121
+ const { imported } = specifier;
122
+ return imported.type === "Identifier" ? imported.name : String(imported.value);
123
+ }
124
+
125
+ // src/transaction-scopes.ts
126
+ var TRANSACTION_METHODS = /* @__PURE__ */ new Set(["transaction", "$transaction", "withTransaction"]);
127
+ var TRANSACTION_DECORATORS = /* @__PURE__ */ new Set(["RelayBusTransaction"]);
128
+ var CONSUMER_DECORATORS = /* @__PURE__ */ new Set(["RelayBusConsumer", "Subscribe"]);
129
+ var CATALOG_CONSUMER_DECORATORS = /* @__PURE__ */ new Set(["RelayBusCatalogConsumer", "EventConsumer"]);
130
+ var CATALOG_HANDLER_NAME = /^on[A-Z]/;
131
+ function isTransactionalMethod(method) {
132
+ if (findDecorator(method, TRANSACTION_DECORATORS) !== void 0) {
133
+ return true;
134
+ }
135
+ const consumer = findDecorator(method, CONSUMER_DECORATORS);
136
+ if (consumer !== void 0 && hasTransactionalOption(consumer)) {
137
+ return true;
138
+ }
139
+ const catalogConsumer = findDecorator(method.parent.parent, CATALOG_CONSUMER_DECORATORS);
140
+ return catalogConsumer !== void 0 && hasTransactionalOption(catalogConsumer) && CATALOG_HANDLER_NAME.test(definitionName(method) ?? "");
141
+ }
142
+ function isTransactionScope(fn) {
143
+ const parent = fn.parent;
144
+ if (parent.type === "MethodDefinition") {
145
+ return isTransactionalMethod(parent);
146
+ }
147
+ if (parent.type !== "CallExpression" || parent.callee.type !== "MemberExpression") {
148
+ return false;
149
+ }
150
+ const method = memberName(parent.callee);
151
+ if (TRANSACTION_METHODS.has(method ?? "")) {
152
+ return parent.arguments[0] === fn;
153
+ }
154
+ return method === "subscribe" && parent.arguments[1] === fn && hasTransactionalOption(parent);
155
+ }
156
+
157
+ // src/transaction-tracker.ts
158
+ var TransactionTracker = class {
159
+ #sourceCode;
160
+ #thisCalls = [];
161
+ #transactional = /* @__PURE__ */ new Map();
162
+ constructor(sourceCode) {
163
+ this.#sourceCode = sourceCode;
164
+ }
165
+ record(call) {
166
+ const { callee } = call;
167
+ if (callee.type !== "MemberExpression" || callee.object.type !== "ThisExpression") {
168
+ return;
169
+ }
170
+ const method = memberName(callee);
171
+ const classBody = this.#ancestors(call).findLast((ancestor) => ancestor.type === "ClassBody");
172
+ if (method !== void 0 && classBody !== void 0) {
173
+ this.#thisCalls.push({ node: call, method, classBody });
174
+ }
175
+ }
176
+ isInTransaction(node) {
177
+ return this.#isInTransaction(node, (classBody) => this.#transactionalMethods(classBody));
178
+ }
179
+ #isInTransaction(node, methodsOf) {
180
+ for (const ancestor of this.#ancestors(node).reverse()) {
181
+ if (isFunction(ancestor) && isTransactionScope(ancestor)) {
182
+ return true;
183
+ }
184
+ if (ancestor.type === "MethodDefinition") {
185
+ return methodsOf(ancestor.parent).has(definitionName(ancestor) ?? "");
186
+ }
187
+ }
188
+ return false;
189
+ }
190
+ #ancestors(node) {
191
+ return this.#sourceCode.getAncestors(node);
192
+ }
193
+ /** Methods of a class that run in a transaction, directly or through `this.method()` calls. */
194
+ #transactionalMethods(classBody) {
195
+ const known = this.#transactional.get(classBody);
196
+ if (known !== void 0) {
197
+ return known;
198
+ }
199
+ const methods = new Set(
200
+ classBody.body.filter((member) => member.type === "MethodDefinition").filter((member) => isTransactionalMethod(member)).map((member) => definitionName(member) ?? "")
201
+ );
202
+ const calls = this.#thisCalls.filter((call) => call.classBody === classBody);
203
+ for (let grew = true; grew; ) {
204
+ const added = calls.filter(
205
+ (call) => !methods.has(call.method) && this.#isInTransaction(call.node, () => methods)
206
+ );
207
+ added.forEach((call) => methods.add(call.method));
208
+ grew = added.length > 0;
209
+ }
210
+ this.#transactional.set(classBody, methods);
211
+ return methods;
212
+ }
213
+ };
214
+
215
+ // src/rules/no-external-call-in-transaction.ts
216
+ var noExternalCallInTransaction = {
217
+ meta: {
218
+ type: "problem",
219
+ docs: {
220
+ description: "Disallow calls to other services inside a database transaction",
221
+ url: "https://berkcansavur.github.io/relaybus/recipes/transactions-in-nestjs"
222
+ },
223
+ schema: [],
224
+ messages: {
225
+ externalCall: "{{call}} calls another service inside a transaction. The transaction keeps its row locks and its database connection until the call returns, and relaybus rolls it back with TransactionTimeoutError once it outlives transactionTimeout. Make the call before the transaction starts or after it commits (for example from the handler of an event you publish)."
226
+ }
227
+ },
228
+ create(context) {
229
+ const tracker = new TransactionTracker(context.sourceCode);
230
+ const detector = new ExternalCallDetector();
231
+ const calls = [];
232
+ return {
233
+ ImportDeclaration: (node) => detector.recordImport(node),
234
+ VariableDeclarator: (node) => detector.recordRequire(node),
235
+ CallExpression: (node) => {
236
+ tracker.record(node);
237
+ calls.push(node);
238
+ },
239
+ "Program:exit": () => {
240
+ for (const call of calls) {
241
+ if (detector.isExternalCall(call) && tracker.isInTransaction(call)) {
242
+ context.report({
243
+ node: call,
244
+ messageId: "externalCall",
245
+ data: { call: `${context.sourceCode.getText(call.callee)}()` }
246
+ });
247
+ }
248
+ }
249
+ }
250
+ };
251
+ }
252
+ };
253
+
254
+ // src/rules/publish-requires-transaction.ts
255
+ var PUBLISH_METHODS = /* @__PURE__ */ new Set(["publish", "publishAll"]);
256
+ var DEFAULT_PUBLISHERS = "(?:^|[a-z_$])(?:[Bb]us|[Pp]ublisher)$|^(?:bus|publisher)$";
257
+ function passesTransaction(call) {
258
+ const options = call.arguments[1];
259
+ if (options === void 0) {
260
+ return false;
261
+ }
262
+ if (options.type !== "ObjectExpression") {
263
+ return true;
264
+ }
265
+ return options.properties.some(
266
+ (property) => property.type === "SpreadElement" || definitionName(property) === "transaction"
267
+ );
268
+ }
269
+ var publishRequiresTransaction = {
270
+ meta: {
271
+ type: "problem",
272
+ docs: {
273
+ description: "Require relaybus publish calls to run inside a transaction",
274
+ url: "https://berkcansavur.github.io/relaybus/recipes/transactions-in-nestjs"
275
+ },
276
+ schema: [
277
+ {
278
+ type: "object",
279
+ properties: { publishers: { type: "string" } },
280
+ additionalProperties: false
281
+ }
282
+ ],
283
+ messages: {
284
+ noTransaction: "{{call}} runs outside a transaction, so the event is not atomic with your writes and relaybus throws TransactionRequiredError. Publish inside a @RelayBusTransaction() method or a bus.transaction(...) callback, or pass { transaction }."
285
+ }
286
+ },
287
+ create(context) {
288
+ const [options = {}] = context.options;
289
+ const publishers = new RegExp(options.publishers ?? DEFAULT_PUBLISHERS);
290
+ const tracker = new TransactionTracker(context.sourceCode);
291
+ const publishes = [];
292
+ return {
293
+ CallExpression: (node) => {
294
+ tracker.record(node);
295
+ const { callee } = node;
296
+ if (callee.type === "MemberExpression" && PUBLISH_METHODS.has(memberName(callee) ?? "") && publishers.test(lastName(callee.object) ?? "")) {
297
+ publishes.push(node);
298
+ }
299
+ },
300
+ "Program:exit": () => {
301
+ for (const call of publishes) {
302
+ if (!passesTransaction(call) && !tracker.isInTransaction(call)) {
303
+ context.report({
304
+ node: call,
305
+ messageId: "noTransaction",
306
+ data: { call: `${context.sourceCode.getText(call.callee)}()` }
307
+ });
308
+ }
309
+ }
310
+ }
311
+ };
312
+ }
313
+ };
314
+
315
+ // src/index.ts
316
+ var rules = {
317
+ "no-external-call-in-transaction": noExternalCallInTransaction,
318
+ "publish-requires-transaction": publishRequiresTransaction
319
+ };
320
+ var plugin = { meta: { name: "@relaybus/eslint-plugin" }, rules };
321
+ var configs = {
322
+ recommended: {
323
+ name: "@relaybus/recommended",
324
+ plugins: { "@relaybus": plugin },
325
+ rules: {
326
+ "@relaybus/no-external-call-in-transaction": "error",
327
+ "@relaybus/publish-requires-transaction": "error"
328
+ }
329
+ }
330
+ };
331
+ var relaybus = { ...plugin, configs };
332
+ var index_default = relaybus;
333
+
334
+ export { configs, index_default as default, rules };
335
+ //# sourceMappingURL=index.js.map
336
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/ast.ts","../src/external-calls.ts","../src/transaction-scopes.ts","../src/transaction-tracker.ts","../src/rules/no-external-call-in-transaction.ts","../src/rules/publish-requires-transaction.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { Rule } from 'eslint';\nimport type * as ESTree from 'estree';\n\n/** A function node as ESLint hands it to rules, with its parent. */\nexport type FunctionNode = Rule.Node &\n (ESTree.FunctionExpression | ESTree.ArrowFunctionExpression | ESTree.FunctionDeclaration);\n\n/** What the TypeScript parser adds to classes and class members. */\ntype Decorated = { readonly decorators?: readonly { readonly expression: ESTree.Node }[] };\n\nconst FUNCTION_TYPES = new Set([\n 'FunctionExpression',\n 'ArrowFunctionExpression',\n 'FunctionDeclaration',\n]);\n\nexport function isFunction(node: Rule.Node): node is FunctionNode {\n return FUNCTION_TYPES.has(node.type);\n}\n\n/** The call of the first decorator of `node` named one of `names`, such as `@RelayBusTransaction()`. */\nexport function findDecorator(\n node: object,\n names: ReadonlySet<string>,\n): ESTree.CallExpression | undefined {\n for (const { expression } of (node as Decorated).decorators ?? []) {\n if (\n expression.type === 'CallExpression' &&\n expression.callee.type === 'Identifier' &&\n names.has(expression.callee.name)\n ) {\n return expression;\n }\n }\n return undefined;\n}\n\n/** Whether an argument of `call` is an object literal with `transactional: true`. */\nexport function hasTransactionalOption(call: ESTree.CallExpression): boolean {\n return call.arguments.some(\n (argument) =>\n argument.type === 'ObjectExpression' &&\n argument.properties.some(\n (property) =>\n property.type === 'Property' &&\n definitionName(property) === 'transactional' &&\n property.value.type === 'Literal' &&\n property.value.value === true,\n ),\n );\n}\n\n/**\n * The name of a property key, class member or import: `name`, `#name` for a private member, the\n * text of a string literal (`'name'`); undefined for anything computed at runtime.\n */\nexport function keyName(key: ESTree.Node): string | undefined {\n if (key.type === 'Identifier') {\n return key.name;\n }\n if (key.type === 'PrivateIdentifier') {\n return `#${key.name}`;\n }\n return key.type === 'Literal' && typeof key.value === 'string' ? key.value : undefined;\n}\n\n/** The member a member expression reads: `get` for `a.get` and `a['get']`, undefined for `a[name]`. */\nexport function memberName(member: ESTree.MemberExpression): string | undefined {\n return staticName(member.computed, member.property);\n}\n\n/** The name of a class member or object property: `place` for `place()` and `['place']()`, undefined for `[name]()`. */\nexport function definitionName(\n definition: ESTree.MethodDefinition | ESTree.Property,\n): string | undefined {\n return staticName(definition.computed, definition.key);\n}\n\nfunction staticName(computed: boolean, key: ESTree.Node): string | undefined {\n return computed && key.type !== 'Literal' ? undefined : keyName(key);\n}\n\n/** The last name of an expression: `bus` for `bus`, `this.bus` and `app.services.bus`. */\nexport function lastName(node: ESTree.Node): string | undefined {\n if (node.type === 'Identifier') {\n return node.name;\n }\n return node.type === 'MemberExpression' ? memberName(node) : undefined;\n}\n","import type { Rule } from 'eslint';\nimport type * as ESTree from 'estree';\nimport { lastName, memberName } from './ast.js';\n\n/** HTTP client modules, by import source. */\ntype ClientModule = 'axios' | 'got' | 'http' | 'undici';\n\nconst MODULES: Readonly<Record<string, ClientModule>> = {\n axios: 'axios',\n got: 'got',\n http: 'http',\n 'node:http': 'http',\n https: 'http',\n 'node:https': 'http',\n undici: 'undici',\n};\n\n/** Members of a module object that send a request; `undefined`: every member does. */\nconst REQUEST_MEMBERS: Readonly<Record<ClientModule, ReadonlySet<string> | undefined>> = {\n axios: undefined,\n got: undefined,\n http: new Set(['request', 'get']),\n undici: new Set(['request', 'fetch', 'stream', 'pipeline']),\n};\n\n/** `HttpService` of `@nestjs/axios`, injected as `httpService` (or `http`), and its `axiosRef`. */\nconst HTTP_SERVICE_NAME = /^(?:http|httpService|axiosRef)$/i;\n\n/**\n * Recognises calls to other services: `fetch`, `axios` and `got` (called or through a method),\n * `http.request` / `https.get` of Node.js, `undici.request` and friends (imported or required),\n * and `HttpService` of `@nestjs/axios`.\n */\nexport class ExternalCallDetector {\n /** Local names of imported HTTP client modules. */\n readonly #modules = new Map<string, ClientModule>([\n ['axios', 'axios'],\n ['got', 'got'],\n ]);\n /** Local names of imported request functions, such as `request` from `undici`. */\n readonly #functions = new Set<string>(['fetch']);\n\n /** Records `import x from 'axios'`, `import * as https from 'node:https'`, `import { request } from 'undici'`. */\n recordImport(declaration: ESTree.ImportDeclaration): void {\n const module = MODULES[String(declaration.source.value)];\n if (module === undefined) {\n return;\n }\n for (const specifier of declaration.specifiers) {\n if (specifier.type !== 'ImportSpecifier') {\n this.#modules.set(specifier.local.name, module);\n } else if (REQUEST_MEMBERS[module]?.has(importedName(specifier)) === true) {\n this.#functions.add(specifier.local.name);\n }\n }\n }\n\n /** Records `const axios = require('axios')`. */\n recordRequire(declarator: ESTree.VariableDeclarator): void {\n const { id, init } = declarator;\n if (\n id.type === 'Identifier' &&\n init?.type === 'CallExpression' &&\n lastName(init.callee) === 'require' &&\n init.arguments[0]?.type === 'Literal'\n ) {\n const module = MODULES[String(init.arguments[0].value)];\n if (module !== undefined) {\n this.#modules.set(id.name, module);\n }\n }\n }\n\n /** Whether `call` sends a request to another service. */\n isExternalCall(call: Rule.Node & ESTree.CallExpression): boolean {\n const { callee } = call;\n if (callee.type === 'Identifier') {\n const module = this.#modules.get(callee.name);\n return this.#functions.has(callee.name) || module === 'axios' || module === 'got';\n }\n return callee.type === 'MemberExpression' && this.#isClientMember(callee);\n }\n\n /** `axios.post`, `https.request`, `this.httpService.get`, `httpService.axiosRef.get`. */\n #isClientMember(callee: ESTree.MemberExpression): boolean {\n if (HTTP_SERVICE_NAME.test(lastName(callee.object) ?? '')) {\n return true;\n }\n const module =\n callee.object.type === 'Identifier' ? this.#modules.get(callee.object.name) : undefined;\n if (module === undefined) {\n return false;\n }\n return REQUEST_MEMBERS[module]?.has(memberName(callee) ?? '') ?? true;\n }\n}\n\n/** `request` for `import { request }` and `import { 'request' as send }`. */\nfunction importedName(specifier: ESTree.ImportSpecifier): string {\n const { imported } = specifier;\n return imported.type === 'Identifier' ? imported.name : String(imported.value);\n}\n","import type { Rule } from 'eslint';\nimport type * as ESTree from 'estree';\nimport {\n findDecorator,\n definitionName,\n hasTransactionalOption,\n memberName,\n type FunctionNode,\n} from './ast.js';\n\n/** Methods whose callback runs in a database transaction: relaybus, TypeORM, Knex, Sequelize, Prisma, MongoDB sessions. */\nconst TRANSACTION_METHODS = new Set(['transaction', '$transaction', 'withTransaction']);\nconst TRANSACTION_DECORATORS = new Set(['RelayBusTransaction']);\n/** Method decorators of relaybus handlers, current name and deprecated alias. */\nconst CONSUMER_DECORATORS = new Set(['RelayBusConsumer', 'Subscribe']);\n/** Class decorators of relaybus catalog handlers, current name and deprecated alias. */\nconst CATALOG_CONSUMER_DECORATORS = new Set(['RelayBusCatalogConsumer', 'EventConsumer']);\nconst CATALOG_HANDLER_NAME = /^on[A-Z]/;\n\n/**\n * Whether `method` runs in a transaction by itself: `@RelayBusTransaction()`, a\n * `@RelayBusConsumer(..., { transactional: true })` handler, or an `on<Event>` method of a class\n * marked `@RelayBusCatalogConsumer(..., { transactional: true })` (or the deprecated names).\n */\nexport function isTransactionalMethod(method: Rule.Node & ESTree.MethodDefinition): boolean {\n if (findDecorator(method, TRANSACTION_DECORATORS) !== undefined) {\n return true;\n }\n const consumer = findDecorator(method, CONSUMER_DECORATORS);\n if (consumer !== undefined && hasTransactionalOption(consumer)) {\n return true;\n }\n // A method's parent is the class body, whose parent is the class.\n const catalogConsumer = findDecorator(method.parent.parent!, CATALOG_CONSUMER_DECORATORS);\n return (\n catalogConsumer !== undefined &&\n hasTransactionalOption(catalogConsumer) &&\n CATALOG_HANDLER_NAME.test(definitionName(method) ?? '')\n );\n}\n\n/**\n * Whether `fn` runs in a transaction: a transactional method, the callback of\n * `x.transaction(fn)` (`bus.transaction`, `dataSource.transaction`, `prisma.$transaction`,\n * `session.withTransaction`), or a handler subscribed with `{ transactional: true }`.\n */\nexport function isTransactionScope(fn: FunctionNode): boolean {\n const parent = fn.parent;\n if (parent.type === 'MethodDefinition') {\n return isTransactionalMethod(parent);\n }\n if (parent.type !== 'CallExpression' || parent.callee.type !== 'MemberExpression') {\n return false;\n }\n const method = memberName(parent.callee);\n if (TRANSACTION_METHODS.has(method ?? '')) {\n return parent.arguments[0] === fn;\n }\n return method === 'subscribe' && parent.arguments[1] === fn && hasTransactionalOption(parent);\n}\n","import type { Rule, SourceCode } from 'eslint';\nimport type * as ESTree from 'estree';\nimport { definitionName, isFunction, memberName } from './ast.js';\nimport { isTransactionScope, isTransactionalMethod } from './transaction-scopes.js';\n\n/** A `this.method()` call and the class body it is written in. */\ntype ThisCall = {\n readonly node: Rule.Node;\n readonly method: string;\n readonly classBody: Rule.Node;\n};\n\n/**\n * Tells whether a node runs inside a transaction. Besides transaction callbacks and transactional\n * methods, a method of the same class that is called with `this.method()` from inside a transaction\n * counts as well, transitively, so helpers of a `@RelayBusTransaction()` method are covered.\n * Record every call with {@link record} while traversing; ask {@link isInTransaction} at `Program:exit`.\n */\nexport class TransactionTracker {\n readonly #sourceCode: SourceCode;\n readonly #thisCalls: ThisCall[] = [];\n readonly #transactional = new Map<Rule.Node, Set<string>>();\n\n constructor(sourceCode: SourceCode) {\n this.#sourceCode = sourceCode;\n }\n\n record(call: Rule.Node & ESTree.CallExpression): void {\n const { callee } = call;\n if (callee.type !== 'MemberExpression' || callee.object.type !== 'ThisExpression') {\n return;\n }\n const method = memberName(callee);\n const classBody = this.#ancestors(call).findLast((ancestor) => ancestor.type === 'ClassBody');\n if (method !== undefined && classBody !== undefined) {\n this.#thisCalls.push({ node: call, method, classBody });\n }\n }\n\n isInTransaction(node: Rule.Node): boolean {\n return this.#isInTransaction(node, (classBody) => this.#transactionalMethods(classBody));\n }\n\n #isInTransaction(node: Rule.Node, methodsOf: (classBody: Rule.Node) => Set<string>): boolean {\n for (const ancestor of this.#ancestors(node).reverse()) {\n if (isFunction(ancestor) && isTransactionScope(ancestor)) {\n return true;\n }\n if (ancestor.type === 'MethodDefinition') {\n return methodsOf(ancestor.parent).has(definitionName(ancestor) ?? '');\n }\n }\n return false;\n }\n\n #ancestors(node: Rule.Node): Rule.Node[] {\n return this.#sourceCode.getAncestors(node) as Rule.Node[];\n }\n\n /** Methods of a class that run in a transaction, directly or through `this.method()` calls. */\n #transactionalMethods(classBody: Rule.Node): Set<string> {\n const known = this.#transactional.get(classBody);\n if (known !== undefined) {\n return known;\n }\n const methods = new Set(\n (classBody as Rule.Node & ESTree.ClassBody).body\n .filter((member) => member.type === 'MethodDefinition')\n .filter((member) => isTransactionalMethod(member as Rule.Node & ESTree.MethodDefinition))\n .map((member) => definitionName(member) ?? ''),\n );\n const calls = this.#thisCalls.filter((call) => call.classBody === classBody);\n for (let grew = true; grew;) {\n const added = calls.filter(\n (call) => !methods.has(call.method) && this.#isInTransaction(call.node, () => methods),\n );\n added.forEach((call) => methods.add(call.method));\n grew = added.length > 0;\n }\n this.#transactional.set(classBody, methods);\n return methods;\n }\n}\n","import type { Rule } from 'eslint';\nimport type * as ESTree from 'estree';\nimport { ExternalCallDetector } from '../external-calls.js';\nimport { TransactionTracker } from '../transaction-tracker.js';\n\n/**\n * Reports calls to other services (HTTP clients) made inside a transaction: a\n * `@RelayBusTransaction()` method, a transactional handler, a `bus.transaction` (or other\n * `.transaction`) callback, and the methods they call through `this`.\n */\nexport const noExternalCallInTransaction: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Disallow calls to other services inside a database transaction',\n url: 'https://berkcansavur.github.io/relaybus/recipes/transactions-in-nestjs',\n },\n schema: [],\n messages: {\n externalCall:\n '{{call}} calls another service inside a transaction. The transaction keeps its row locks ' +\n 'and its database connection until the call returns, and relaybus rolls it back with ' +\n 'TransactionTimeoutError once it outlives transactionTimeout. Make the call before the ' +\n 'transaction starts or after it commits (for example from the handler of an event you publish).',\n },\n },\n create(context) {\n const tracker = new TransactionTracker(context.sourceCode);\n const detector = new ExternalCallDetector();\n const calls: (Rule.Node & ESTree.CallExpression)[] = [];\n return {\n ImportDeclaration: (node) => detector.recordImport(node),\n VariableDeclarator: (node) => detector.recordRequire(node),\n CallExpression: (node) => {\n tracker.record(node);\n calls.push(node);\n },\n 'Program:exit': () => {\n for (const call of calls) {\n if (detector.isExternalCall(call) && tracker.isInTransaction(call)) {\n context.report({\n node: call,\n messageId: 'externalCall',\n data: { call: `${context.sourceCode.getText(call.callee)}()` },\n });\n }\n }\n },\n };\n },\n};\n","import type { Rule } from 'eslint';\nimport type * as ESTree from 'estree';\nimport { definitionName, lastName, memberName } from '../ast.js';\nimport { TransactionTracker } from '../transaction-tracker.js';\n\nconst PUBLISH_METHODS = new Set(['publish', 'publishAll']);\n/** Default names of the objects whose `publish` is a relaybus publish: `bus`, `this.eventBus`, `orderPublisher`. */\nconst DEFAULT_PUBLISHERS = '(?:^|[a-z_$])(?:[Bb]us|[Pp]ublisher)$|^(?:bus|publisher)$';\n\ntype Options = { readonly publishers?: string };\n\n/**\n * Whether `call` passes a transaction itself: `{ transaction }` in its options, or options the\n * rule cannot see into (a variable, a spread).\n */\nfunction passesTransaction(call: ESTree.CallExpression): boolean {\n const options = call.arguments[1];\n if (options === undefined) {\n return false;\n }\n if (options.type !== 'ObjectExpression') {\n return true;\n }\n return options.properties.some(\n (property) => property.type === 'SpreadElement' || definitionName(property) === 'transaction',\n );\n}\n\n/**\n * Reports `bus.publish` / `bus.publishAll` outside a transaction: not in a\n * `@RelayBusTransaction()` method, a transactional handler or a `.transaction` callback (or a\n * method they call through `this`), and without `{ transaction }`.\n */\nexport const publishRequiresTransaction: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Require relaybus publish calls to run inside a transaction',\n url: 'https://berkcansavur.github.io/relaybus/recipes/transactions-in-nestjs',\n },\n schema: [\n {\n type: 'object',\n properties: { publishers: { type: 'string' } },\n additionalProperties: false,\n },\n ],\n messages: {\n noTransaction:\n '{{call}} runs outside a transaction, so the event is not atomic with your writes and ' +\n 'relaybus throws TransactionRequiredError. Publish inside a @RelayBusTransaction() method ' +\n 'or a bus.transaction(...) callback, or pass { transaction }.',\n },\n },\n create(context) {\n const [options = {}] = context.options as [Options?];\n const publishers = new RegExp(options.publishers ?? DEFAULT_PUBLISHERS);\n const tracker = new TransactionTracker(context.sourceCode);\n const publishes: (Rule.Node & ESTree.CallExpression)[] = [];\n return {\n CallExpression: (node) => {\n tracker.record(node);\n const { callee } = node;\n if (\n callee.type === 'MemberExpression' &&\n PUBLISH_METHODS.has(memberName(callee) ?? '') &&\n publishers.test(lastName(callee.object) ?? '')\n ) {\n publishes.push(node);\n }\n },\n 'Program:exit': () => {\n for (const call of publishes) {\n if (!passesTransaction(call) && !tracker.isInTransaction(call)) {\n context.report({\n node: call,\n messageId: 'noTransaction',\n data: { call: `${context.sourceCode.getText(call.callee)}()` },\n });\n }\n }\n },\n };\n },\n};\n","import type { ESLint, Linter, Rule } from 'eslint';\nimport { noExternalCallInTransaction } from './rules/no-external-call-in-transaction.js';\nimport { publishRequiresTransaction } from './rules/publish-requires-transaction.js';\n\nexport const rules: Readonly<Record<string, Rule.RuleModule>> = {\n 'no-external-call-in-transaction': noExternalCallInTransaction,\n 'publish-requires-transaction': publishRequiresTransaction,\n};\n\nconst plugin = { meta: { name: '@relaybus/eslint-plugin' }, rules } satisfies ESLint.Plugin;\n\n/**\n * Both rules at error level, under the plugin name `@relaybus`:\n *\n * ```js\n * // eslint.config.js\n * import relaybus from '@relaybus/eslint-plugin';\n * export default [relaybus.configs.recommended];\n * ```\n */\nexport const configs: { readonly recommended: Linter.Config } = {\n recommended: {\n name: '@relaybus/recommended',\n plugins: { '@relaybus': plugin },\n rules: {\n '@relaybus/no-external-call-in-transaction': 'error',\n '@relaybus/publish-requires-transaction': 'error',\n },\n },\n};\n\nconst relaybus: ESLint.Plugin & { readonly configs: typeof configs } = { ...plugin, configs };\n\nexport default relaybus;\n"]}
package/package.json CHANGED
@@ -1,6 +1,67 @@
1
1
  {
2
2
  "name": "@relaybus/eslint-plugin",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.3.0",
4
+ "description": "ESLint rules for relaybus: no calls to other services inside a transaction, and no publish outside a transaction, so outbox transactions stay short and events stay atomic with your writes.",
5
+ "license": "MIT",
6
+ "author": "Berkcan Şavur <berkcansavur@gmail.com>",
7
+ "homepage": "https://berkcansavur.github.io/relaybus/packages/eslint-plugin",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/berkcansavur/relaybus.git",
11
+ "directory": "packages/eslint-plugin"
12
+ },
13
+ "bugs": "https://github.com/berkcansavur/relaybus/issues",
14
+ "keywords": [
15
+ "relaybus",
16
+ "eslint",
17
+ "eslintplugin",
18
+ "eslint-plugin",
19
+ "transactional-outbox",
20
+ "outbox-pattern",
21
+ "nestjs",
22
+ "transactions",
23
+ "typescript"
24
+ ],
25
+ "type": "module",
26
+ "sideEffects": false,
27
+ "main": "./dist/index.cjs",
28
+ "module": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "import": {
33
+ "types": "./dist/index.d.ts",
34
+ "default": "./dist/index.js"
35
+ },
36
+ "require": {
37
+ "types": "./dist/index.d.cts",
38
+ "default": "./dist/index.cjs"
39
+ }
40
+ },
41
+ "./package.json": "./package.json"
42
+ },
43
+ "files": [
44
+ "dist",
45
+ "README.md",
46
+ "LICENSE"
47
+ ],
48
+ "engines": {
49
+ "node": ">=22.12.0"
50
+ },
51
+ "publishConfig": {
52
+ "access": "public",
53
+ "provenance": true
54
+ },
55
+ "peerDependencies": {
56
+ "eslint": ">=9.0.0"
57
+ },
58
+ "devDependencies": {
59
+ "@types/estree": "1.0.9",
60
+ "eslint": "9.39.5",
61
+ "typescript-eslint": "8.71.2"
62
+ },
63
+ "scripts": {
64
+ "build": "tsup",
65
+ "typecheck": "tsc -p tsconfig.build.json --noEmit"
66
+ }
6
67
  }