Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ Fail-closed and doc findings from the 3.5.0 adversarial review.
- **A count operator across a bridge says so**: "'crm:Event' crosses a bridge: count operators
aren't supported across a bridge", not "is not a relation … Count operators require a relation
field".
- **`validateRule` requires a count operator's `condition` and `count` on every target.** It
required them only for `check`, so `{ field: 'posts', arrayOperator: 'atLeast', count: 2 }`
validated for `toPrisma` (and a lens source holding it passed `validateNarrowing`) while the
compile, like `check`, refuses it. Now `missing_condition` / `missing_count` on every target, so
`validateNarrowing` and `toSourceQueries` agree.
- README: both source-query snippets destructure `query.prisma` and run `steps` through
`executePrismaPlan` (the bridge snippet called `prisma[query.model].findMany(query.prisma)`,
which Prisma rejects); a test runs that shape against the rails harness. `orderRecords` notes
Expand Down
5 changes: 3 additions & 2 deletions src/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -789,7 +789,8 @@ const validateArrayRule = (
);
refuseCount();
} else if (shape === 'count') {
if (context.target !== 'toPrisma' && typeof rule.count !== 'number')
// Every rail requires both (check, and toPrisma's count step; toSql refuses the operator).
if (typeof rule.count !== 'number')
pushIssue(context, `${path}.count`, 'missing_count', countRequired(operator).message);
else if (
rule.count !== undefined &&
Expand All @@ -804,7 +805,7 @@ const validateArrayRule = (
if (hasCondition) {
enterScope(context, depth, rule.field, true);
validateCondition(rule.condition, `${path}.condition`, context, depth + 1);
} else if (context.target === 'check')
} else
pushIssue(
context,
`${path}.condition`,
Expand Down
30 changes: 30 additions & 0 deletions test/review.351.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
toLensSelect,
toSourceQueries,
validateNarrowing,
validateRule,
} from '../index';
import { map, openRails } from './rails/harness';

Expand Down Expand Up @@ -309,3 +310,32 @@ describe('the README source-query snippet runs as written', () => {
expect(options.map((o) => o.value)).toEqual(truth);
});
});

describe('a count operator needs a condition and a count on every target', () => {
const noCondition = rule({ field: 'posts', arrayOperator: 'atLeast', count: 2 });
const noCount = rule({
field: 'posts',
arrayOperator: 'atLeast',
condition: { field: 'views', operator: 'greaterThan', value: 0 },
});

test.each(['check', 'toPrisma', 'toSql'] as const)('validateRule for %s refuses', (target) => {
const options = { target, map, model: 'User' };
expect(validateRule(noCondition, options).errors.map((e) => e.code)).toContain(
'missing_condition',
);
expect(validateRule(noCount, options).errors.map((e) => e.code)).toContain('missing_count');
});

test.each([
['no condition', noCondition],
['no count', noCount],
])('a source with %s: validateNarrowing and toSourceQueries agree', (_, where) => {
const lens: LensNarrowing = {
parent: users,
root: { relations: { posts: {} }, sources: { name: where } },
};
expect(validateNarrowing(lens).ok).toBe(false);
expect(() => toSourceQueries(lens)).toThrow();
});
});
7 changes: 5 additions & 2 deletions test/validateRule.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ describe('validateRule', () => {
expect(result.errors.map((error) => error.code)).toContain('missing_count');
});

test('allows missing count for toPrisma count operators', () => {
test('rejects a missing count and condition for toPrisma count operators too', () => {
const result = validateRule(
{
field: 'posts',
Expand All @@ -82,7 +82,10 @@ describe('validateRule', () => {
{ target: 'toPrisma' },
);

expect(result).toEqual({ ok: true, errors: [] });
expect(result.errors.map((error) => error.code).sort()).toEqual([
'missing_condition',
'missing_count',
]);
});

test('rejects prisma-incompatible operators', () => {
Expand Down
Loading