Files
zeekayandHanzo Dev 1c7d2f6fbb fix(lint): read the config the way ESLint 10 reads one
ESLint was raised to 10, which reads eslint.config.* and nothing else. This
repo still said it all in .eslintrc.cjs and .eslintignore, so every lint run
ended at "couldn't find an eslint.config file" — and the pre-commit hook runs
eslint over staged sources, so no commit touching a .ts file could be made.

The config is the same config in the flat spelling: every rule, every ignore
and the __tests__ override carried over, and the two legacy files are gone
rather than left beside it to disagree.

The transaction rule (inside $transaction, only `tx`) keeps both its callers.
transaction.cjs is a whole flat config, so `lint:tx` still hands it to eslint
alone — with --no-config-lookup, since --no-eslintrc and --ext no longer
exist — and the shared config spreads it in. Its test now gives the linter
the array AS SHIPPED instead of re-stating the parser beside a copy of the
rules: a copy keeps passing after the original stops selecting files. 10 of
10, and `lint:tx` is clean over packages and apps.

eslint-plugin-react finds the React version by asking the rule context for a
filename through an API ESLint 10 removed, so detection threw on the first
file. The version is stated, which never takes that path.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-18 17:58:11 -07:00

138 lines
4.7 KiB
JavaScript

const assert = require('node:assert/strict');
const { test } = require('node:test');
const { Linter } = require('eslint');
const config = require('../transaction.cjs');
// The config AS SHIPPED, handed to the linter whole: the same array `lint:tx`
// gives eslint. Reading the rules out of it and re-stating the parser here would
// test a copy, and a copy keeps passing after the original stops selecting files.
const linter = new Linter({ configType: 'flat' });
/** @param {string} code */
const check = (code) => linter.verify(code, config, { filename: 'probe.ts' }).map((m) => m.message);
test('a transaction that only touches tx is clean', () => {
assert.deepEqual(
check(`
const envelope = await prisma.$transaction(async (tx) => {
const created = await tx.envelope.create({ data });
await tx.recipient.updateMany({ where: { envelopeId: created.id }, data });
return created;
}, { timeout: 30_000 });
await jobs.triggerJob({ name: 'send.signing.requested.email', payload });
await triggerWebhook({ event: 'DOCUMENT_CREATED', data: envelope });
await mailer.sendMail({ to, from, subject, html, text });
`),
[],
);
});
test('the shapes that carry tx are clean', () => {
assert.deepEqual(
check(`
await prisma.$transaction(async (tx) => {
await tx.$executeRaw\`pragma foreign_keys = on\`;
await tx.documentAuditLog.createMany({ data: auditLogsToCreate });
await Promise.all([tx.field.create({ data }), tx.recipient.create({ data })]);
await Promise.all(items.map(async (item) => tx.field.create({ data: item })));
await validateFieldAuth({ tx, field });
await tx.envelope.create({ data }).then((created) => created.id);
const id = generateDatabaseId('team_group');
const value = match(field.type).with(FieldType.DATE, () => date).otherwise(() => null);
});
`),
[],
);
});
test('the batch form is not an interactive transaction', () => {
assert.deepEqual(
check('await prisma.$transaction([prisma.a.findMany(), prisma.b.findMany()]);'),
[],
);
});
test('a global prisma read inside a transaction is an error', () => {
const messages = check(`
await prisma.$transaction(async (tx) => {
return await prisma.recipient.findFirst({ where });
});
`);
assert.ok(messages.some((m) => m.startsWith('no global prisma inside $transaction')));
});
test('a nested transaction is an error', () => {
const messages = check(`
await prisma.$transaction(async (tx) => {
await prisma.$transaction(async (inner) => inner.member.create({ data }));
});
`);
assert.ok(messages.some((m) => m.startsWith('no global prisma inside $transaction')));
});
test('a job trigger inside a transaction is an error, whatever the client is called', () => {
for (const client of ['jobs', 'jobsClient']) {
const [message, ...rest] = check(`
await prisma.$transaction(async (tx) => {
await tx.team.delete({ where });
await ${client}.triggerJob({ name: 'send.team-deleted.email', payload });
});
`);
assert.equal(rest.length, 0);
assert.match(message, /^no awaiting a call you did not hand tx inside \$transaction/);
}
});
test('a webhook inside a transaction is an error', () => {
const [message, ...rest] = check(`
await prisma.$transaction(async (tx) => {
const created = await tx.envelope.create({ data });
await triggerWebhook({ event: 'DOCUMENT_CREATED', data: created });
});
`);
assert.equal(rest.length, 0);
assert.match(message, /^no awaiting a call you did not hand tx inside \$transaction/);
});
test('an email inside a transaction is an error', () => {
const [message, ...rest] = check(`
await prisma.$transaction(async (tx) => {
await mailer.sendMail({ to, from, subject, html, text });
await tx.documentAuditLog.create({ data });
});
`);
assert.equal(rest.length, 0);
assert.match(message, /^no awaiting a call you did not hand tx inside \$transaction/);
});
test('any other call out of the process inside a transaction is an error', () => {
const [message, ...rest] = check(`
await prisma.$transaction(async (tx) => {
await verifyDomainWithDKIM(domain, selector, privateKey).catch((err) => { throw err; });
return await tx.emailDomain.create({ data });
});
`);
assert.equal(rest.length, 0);
assert.match(message, /^no awaiting a call you did not hand tx inside \$transaction/);
});
test('a transaction client by any other name is an error', () => {
const messages = check(`
await prisma.$transaction(async (trx) => {
await trx.envelope.create({ data });
});
`);
assert.ok(messages.some((m) => m.startsWith('name the transaction client tx')));
});