History

Fix storage test boundary lint

Rhys Sullivan committed 6 months ago

Commit 89bc9c91 parent b039b6e2 files · +137 −180Browse files

Unified · Split

2 files
  1. packages/core/storage-core/src/testing/conformance.ts +120 −138
  2. packages/core/storage-core/src/testing/memory.ts +17 −42
packages/core/storage-core/src/testing/conformance.ts+120−138
@@ -13,7 +13,7 @@ · 12 unmodified lines
1313
1414 import { describe, it } from "@effect/vitest";
1515 import { expect } from "@effect/vitest";
16−import { Effect, Result } from "effect";
16+import { Data, Effect, Result } from "effect";
1717
1818 import type { DBAdapter } from "../adapter";
1919 import type { DBSchema } from "../schema";
@@ -75,14 +75,13 @@ export type WithAdapter = <A, E>( · 55 unmodified lines
7575 fn: (adapter: DBAdapter) => Effect.Effect<A, E>,
7676 ) => Effect.Effect<A, E | Error>;
7777
78+class TransactionRollbackTestError extends Data.TaggedError("TransactionRollbackTestError")<{}> {}
79+
7880 // ---------------------------------------------------------------------------
7981 // Suite
8082 // ---------------------------------------------------------------------------
8183
82−export const runAdapterConformance = (
83− name: string,
84− withAdapter: WithAdapter,
85−): void => {
84+export const runAdapterConformance = (name: string, withAdapter: WithAdapter): void => {
8685 describe(`conformance: ${name}`, () => {
8786 const withDefaultsInput = (
8887 value: unknown,
@@ -136,9 +135,7 @@ export const runAdapterConformance = ( · 47 unmodified lines
136135 expect(found!.id).toBe(created.id);
137136 expect(found!.enabled).toBe(true);
138137 expect(found!.createdAt instanceof Date).toBe(true);
139− expect(found!.createdAt.toISOString()).toBe(
140− "2026-04-15T00:00:00.000Z",
141− );
138+ expect(found!.createdAt.toISOString()).toBe("2026-04-15T00:00:00.000Z");
142139 expect(found!.metadata).toEqual({ slug: "gh", tags: ["a", "b"] });
143140 }),
144141 ),
@@ -161,7 +158,7 @@ export const runAdapterConformance = ( · 16 unmodified lines
161158 });
162159 const found = yield* adapter.findOne<{ createdAt: Date }>({
163160 model: "source",
164− where: [{ field: "id", value: row.id as string }],
161+ where: [{ field: "id", value: row.id }],
165162 });
166163 expect(found!.createdAt.toISOString()).toBe(d.toISOString());
167164 }),
@@ -381,10 +378,7 @@ export const runAdapterConformance = ( · 213 unmodified lines
381378 model: "source",
382379 where: [{ field: "priority", value: 5, operator: "gte" }],
383380 });
384− expect(highPriority.map((r) => r.name).sort()).toEqual([
385− "github-edge",
386− "gitlab",
387− ]);
381+ expect(highPriority.map((r) => r.name).sort()).toEqual(["github-edge", "gitlab"]);
388382 }),
389383 ),
390384 );
@@ -516,9 +510,7 @@ export const runAdapterConformance = ( · 125 unmodified lines
516510 update: { touchedAt: explicitDate },
517511 });
518512 expect(updated).not.toBeNull();
519− expect(updated!.touchedAt.toISOString()).toBe(
520− explicitDate.toISOString(),
521− );
513+ expect(updated!.touchedAt.toISOString()).toBe(explicitDate.toISOString());
522514 // Sanity: omitting touchedAt should trigger onUpdate.
523515 const hookDriven = yield* adapter.update<{
524516 id: string;
@@ -530,9 +522,7 @@ export const runAdapterConformance = ( · 5 unmodified lines
530522 update: { name: "rename" },
531523 });
532524 expect(hookDriven).not.toBeNull();
533− expect(hookDriven!.touchedAt.toISOString()).toBe(
534− "2099-01-01T00:00:00.000Z",
535− );
525+ expect(hookDriven!.touchedAt.toISOString()).toBe("2099-01-01T00:00:00.000Z");
536526 }),
537527 ),
538528 );
@@ -546,7 +536,7 @@ export const runAdapterConformance = ( · 7 unmodified lines
546536 Effect.gen(function* () {
547537 yield* trx.create({ model: "tag", data: { label: "tx1" } });
548538 yield* trx.create({ model: "tag", data: { label: "tx2" } });
549− return yield* Effect.fail(new Error("boom"));
539+ return yield* new TransactionRollbackTestError();
550540 }),
551541 )
552542 .pipe(Effect.result);
@@ -571,128 +561,120 @@ export const runAdapterConformance = ( · 18 unmodified lines
571561 ),
572562 );
573563
574− it.effect(
575− "where: mixed AND/OR grouping follows upstream split-group semantics",
576− () =>
577− // Locks in better-auth drizzle adapter's `convertWhereClause`
578− // semantics: AND-connector clauses and OR-connector clauses split
579− // into two groups, recombined as `(AND…) AND (OR…)`. For
580− // [{priority=1, AND}, {priority=10, OR}, {enabled=true, AND}],
581− // that's `(priority=1 AND enabled=true) AND (priority=10)` which
582− // can never match a single row — while a left-to-right fold
583− // would give `((priority=1 OR priority=10) AND enabled=true)`
584− // and return two rows. We assert the upstream reading.
585− withAdapter((adapter) =>
586− Effect.gen(function* () {
587− yield* adapter.createMany({
588− model: "source",
589− data: [
590− { name: "lhs", priority: 1, enabled: true },
591− { name: "rhs", priority: 10, enabled: true },
592− { name: "off", priority: 1, enabled: false },
593− ],
594− });
595− const rows = yield* adapter.findMany<{ name: string }>({
596− model: "source",
597− where: [
598− { field: "priority", value: 1, connector: "AND" },
599− { field: "priority", value: 10, connector: "OR" },
600− { field: "enabled", value: true, connector: "AND" },
601− ],
602− });
603− // Upstream split-group: (priority=1 AND enabled=true) AND
604− // (priority=10). `lhs` has priority=1 (fails the OR group's
605− // priority=10 check) and `rhs` has priority=10 (fails the
606− // AND group's priority=1 check) — both reject.
607− expect(rows.map((r) => r.name)).toEqual([]);
608−
609− // Sanity: a pure disjunction still works.
610− const both = yield* adapter.findMany<{ name: string }>({
611− model: "source",
612− where: [
613− { field: "priority", value: 1, connector: "OR" },
614− { field: "priority", value: 10, connector: "OR" },
615− ],
616− sortBy: { field: "name", direction: "asc" },
617− });
618− expect(both.map((r) => r.name)).toEqual(["lhs", "off", "rhs"]);
619− }),
620− ),
564+ it.effect("where: mixed AND/OR grouping follows upstream split-group semantics", () =>
565+ // Locks in better-auth drizzle adapter's `convertWhereClause`
566+ // semantics: AND-connector clauses and OR-connector clauses split
567+ // into two groups, recombined as `(AND…) AND (OR…)`. For
568+ // [{priority=1, AND}, {priority=10, OR}, {enabled=true, AND}],
569+ // that's `(priority=1 AND enabled=true) AND (priority=10)` which
570+ // can never match a single row — while a left-to-right fold
571+ // would give `((priority=1 OR priority=10) AND enabled=true)`
572+ // and return two rows. We assert the upstream reading.
573+ withAdapter((adapter) =>
574+ Effect.gen(function* () {
575+ yield* adapter.createMany({
576+ model: "source",
577+ data: [
578+ { name: "lhs", priority: 1, enabled: true },
579+ { name: "rhs", priority: 10, enabled: true },
580+ { name: "off", priority: 1, enabled: false },
581+ ],
582+ });
583+ const rows = yield* adapter.findMany<{ name: string }>({
584+ model: "source",
585+ where: [
586+ { field: "priority", value: 1, connector: "AND" },
587+ { field: "priority", value: 10, connector: "OR" },
588+ { field: "enabled", value: true, connector: "AND" },
589+ ],
590+ });
591+ // Upstream split-group: (priority=1 AND enabled=true) AND
592+ // (priority=10). `lhs` has priority=1 (fails the OR group's
593+ // priority=10 check) and `rhs` has priority=10 (fails the
594+ // AND group's priority=1 check) — both reject.
595+ expect(rows.map((r) => r.name)).toEqual([]);
596+
597+ // Sanity: a pure disjunction still works.
598+ const both = yield* adapter.findMany<{ name: string }>({
599+ model: "source",
600+ where: [
601+ { field: "priority", value: 1, connector: "OR" },
602+ { field: "priority", value: 10, connector: "OR" },
603+ ],
604+ sortBy: { field: "name", direction: "asc" },
605+ });
606+ expect(both.map((r) => r.name)).toEqual(["lhs", "off", "rhs"]);
607+ }),
608+ ),
621609 );
622610
623− it.effect(
624− "findMany resolves join: source → source_tag (one-to-many)",
625− () =>
626− withAdapter((adapter) =>
627− Effect.gen(function* () {
628− const src = yield* adapter.create<{ id: string; name: string }>({
629− model: "source",
630− data: { name: "joined-source" },
631− });
632− yield* adapter.createMany({
633− model: "source_tag",
634− data: [
635− { sourceId: src.id, note: "first" },
636− { sourceId: src.id, note: "second" },
637− ],
638− });
639−
640− const many = yield* adapter.findMany<{
641− id: string;
642− name: string;
643− source_tag: ReadonlyArray<{ note: string; sourceId: string }>;
644− }>({
645− model: "source",
646− where: [{ field: "id", value: src.id }],
647− join: { source_tag: true },
648− });
649− expect(many).toHaveLength(1);
650− const parent = many[0]!;
651− expect(parent.name).toBe("joined-source");
652− expect(Array.isArray(parent.source_tag)).toBe(true);
653− expect(parent.source_tag).toHaveLength(2);
654− expect(
655− parent.source_tag.map((t) => t.note).sort(),
656− ).toEqual(["first", "second"]);
657− }),
658− ),
611+ it.effect("findMany resolves join: source → source_tag (one-to-many)", () =>
612+ withAdapter((adapter) =>
613+ Effect.gen(function* () {
614+ const src = yield* adapter.create<{ id: string; name: string }>({
615+ model: "source",
616+ data: { name: "joined-source" },
617+ });
618+ yield* adapter.createMany({
619+ model: "source_tag",
620+ data: [
621+ { sourceId: src.id, note: "first" },
622+ { sourceId: src.id, note: "second" },
623+ ],
624+ });
625+
626+ const many = yield* adapter.findMany<{
627+ id: string;
628+ name: string;
629+ source_tag: ReadonlyArray<{ note: string; sourceId: string }>;
630+ }>({
631+ model: "source",
632+ where: [{ field: "id", value: src.id }],
633+ join: { source_tag: true },
634+ });
635+ expect(many).toHaveLength(1);
636+ const parent = many[0]!;
637+ expect(parent.name).toBe("joined-source");
638+ expect(Array.isArray(parent.source_tag)).toBe(true);
639+ expect(parent.source_tag).toHaveLength(2);
640+ expect(parent.source_tag.map((t) => t.note).sort()).toEqual(["first", "second"]);
641+ }),
642+ ),
659643 );
660644
661− it.effect(
662− "findOne resolves join: source_tag → source (one-to-one)",
663− () =>
664− withAdapter((adapter) =>
665− Effect.gen(function* () {
666− const src = yield* adapter.create<{ id: string; name: string }>({
667− model: "source",
668− data: { name: "owner" },
669− });
670− const child = yield* adapter.create<{
671− id: string;
672− sourceId: string;
673− note: string;
674− }>({
675− model: "source_tag",
676− data: { sourceId: src.id, note: "only" },
677− });
678−
679− const found = yield* adapter.findOne<{
680− id: string;
681− note: string;
682− sourceId: string;
683− source: { id: string; name: string } | null;
684− }>({
685− model: "source_tag",
686− where: [{ field: "id", value: child.id }],
687− join: { source: true },
688− });
689− expect(found).not.toBeNull();
690− expect(found!.note).toBe("only");
691− expect(found!.source).not.toBeNull();
692− expect(found!.source!.id).toBe(src.id);
693− expect(found!.source!.name).toBe("owner");
694− }),
695− ),
645+ it.effect("findOne resolves join: source_tag → source (one-to-one)", () =>
646+ withAdapter((adapter) =>
647+ Effect.gen(function* () {
648+ const src = yield* adapter.create<{ id: string; name: string }>({
649+ model: "source",
650+ data: { name: "owner" },
651+ });
652+ const child = yield* adapter.create<{
653+ id: string;
654+ sourceId: string;
655+ note: string;
656+ }>({
657+ model: "source_tag",
658+ data: { sourceId: src.id, note: "only" },
659+ });
660+
661+ const found = yield* adapter.findOne<{
662+ id: string;
663+ note: string;
664+ sourceId: string;
665+ source: { id: string; name: string } | null;
666+ }>({
667+ model: "source_tag",
668+ where: [{ field: "id", value: child.id }],
669+ join: { source: true },
670+ });
671+ expect(found).not.toBeNull();
672+ expect(found!.note).toBe("only");
673+ expect(found!.source).not.toBeNull();
674+ expect(found!.source!.id).toBe(src.id);
675+ expect(found!.source!.name).toBe("owner");
676+ }),
677+ ),
696678 );
697679
698680 it.effect("nested writes inside a transaction see the tx state", () =>
packages/core/storage-core/src/testing/memory.ts+17−42
@@ -31,11 +31,7 @@ type Row = Record<string, unknown>; · 30 unmodified lines
3131 type Store = Record<string, Row[]>;
3232 type Comparable = string | number | boolean | Date;
3333
34−const compare = (
35− a: unknown,
36− b: unknown,
37− op: "gt" | "gte" | "lt" | "lte",
38−): boolean => {
34+const compare = (a: unknown, b: unknown, op: "gt" | "gte" | "lt" | "lte"): boolean => {
3935 if (
4036 !(
4137 typeof a === "string" ||
@@ -43,12 +39,7 @@ const compare = ( · 1 unmodified line
4339 typeof a === "boolean" ||
4440 a instanceof Date
4541 ) ||
46− !(
47− typeof b === "string" ||
48− typeof b === "number" ||
49− typeof b === "boolean" ||
50− b instanceof Date
51− )
42+ !(typeof b === "string" || typeof b === "number" || typeof b === "boolean" || b instanceof Date)
5243 ) {
5344 return false;
5445 }
@@ -74,27 +65,25 @@ const evalClause = (record: Row, clause: CleanedWhere): boolean => { · 19 unmodified lines
7465 const isInsensitive =
7566 mode === "insensitive" &&
7667 (typeof value === "string" ||
77− (Array.isArray(value) &&
78− (value as unknown[]).every((v) => typeof v === "string")));
68+ (Array.isArray(value) && (value as unknown[]).every((v) => typeof v === "string")));
7969
8070 const lhs = record[field];
81− const lowerStr = (v: unknown) =>
82− typeof v === "string" ? v.toLowerCase() : v;
71+ const lowerStr = (v: unknown) => (typeof v === "string" ? v.toLowerCase() : v);
8372
8473 const cmp = (a: unknown, b: unknown): boolean =>
8574 isInsensitive ? lowerStr(a) === lowerStr(b) : a === b;
8675 switch (operator) {
8776 case "in":
77+ // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: sync test adapter predicate preserves invalid where-clause failure semantics
8878 if (!Array.isArray(value)) throw new Error("Value must be an array");
8979 return (value as unknown[]).some((v) => cmp(lhs, v));
9080 case "not_in":
81+ // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: sync test adapter predicate preserves invalid where-clause failure semantics
9182 if (!Array.isArray(value)) throw new Error("Value must be an array");
9283 return !(value as unknown[]).some((v) => cmp(lhs, v));
9384 case "contains": {
9485 if (typeof lhs !== "string" || typeof value !== "string") return false;
95− return isInsensitive
96− ? lhs.toLowerCase().includes(value.toLowerCase())
97− : lhs.includes(value);
86+ return isInsensitive ? lhs.toLowerCase().includes(value.toLowerCase()) : lhs.includes(value);
9887 }
9988 case "starts_with": {
10089 if (typeof lhs !== "string" || typeof value !== "string") return false;
@@ -104,9 +93,7 @@ const evalClause = (record: Row, clause: CleanedWhere): boolean => { · 3 unmodified lines
10493 }
10594 case "ends_with": {
10695 if (typeof lhs !== "string" || typeof value !== "string") return false;
107− return isInsensitive
108− ? lhs.toLowerCase().endsWith(value.toLowerCase())
109− : lhs.endsWith(value);
96+ return isInsensitive ? lhs.toLowerCase().endsWith(value.toLowerCase()) : lhs.endsWith(value);
11097 }
11198 case "ne":
11299 return !cmp(lhs, value);
@@ -135,14 +122,10 @@ const evalClause = (record: Row, clause: CleanedWhere): boolean => { · 22 unmodified lines
135122 const matchAll = (record: Row, where: readonly CleanedWhere[]): boolean => {
136123 if (where.length === 0) return true;
137124 if (where.length === 1) return evalClause(record, where[0]!);
138− const andGroup = where.filter(
139− (w) => w.connector === "AND" || !w.connector,
140− );
125+ const andGroup = where.filter((w) => w.connector === "AND" || !w.connector);
141126 const orGroup = where.filter((w) => w.connector === "OR");
142− const andResult =
143− andGroup.length === 0 ? true : andGroup.every((w) => evalClause(record, w));
144− const orResult =
145− orGroup.length === 0 ? true : orGroup.some((w) => evalClause(record, w));
127+ const andResult = andGroup.length === 0 ? true : andGroup.every((w) => evalClause(record, w));
128+ const orResult = orGroup.length === 0 ? true : orGroup.some((w) => evalClause(record, w));
146129 return andResult && orResult;
147130 };
148131
@@ -167,9 +150,7 @@ export interface MakeMemoryAdapterOptions { · 18 unmodified lines
167150 readonly generateId?: () => string;
168151 }
169152
170−export const makeMemoryAdapter = (
171− options: MakeMemoryAdapterOptions,
172−): DBAdapter => {
153+export const makeMemoryAdapter = (options: MakeMemoryAdapterOptions): DBAdapter => {
173154 let store: Store = {};
174155
175156 const tableFor = (model: string): Row[] => {
@@ -186,9 +167,7 @@ export const makeMemoryAdapter = ( · 10 unmodified lines
186167 const out: Row = { ...base };
187168 for (const [target, cfg] of Object.entries(join)) {
188169 const targetRows = tableFor(target);
189− const matches = targetRows.filter(
190− (r) => r[cfg.on.to] === base[cfg.on.from],
191− );
170+ const matches = targetRows.filter((r) => r[cfg.on.to] === base[cfg.on.from]);
192171 if (cfg.relation === "one-to-one") {
193172 out[target] = matches[0] ?? null;
194173 } else {
@@ -328,11 +307,9 @@ export const makeMemoryAdapter = ( · 133 unmodified lines
328307
329308 // Snapshot-based transaction: clone on entry, restore on failure.
330309 const txFn: DBAdapterFactoryConfig["transaction"] = <R, E>(
331− cb: (trx: Parameters<DBAdapter["transaction"]>[0] extends (
332− t: infer T,
333− ) => unknown
334− ? T
335− : never) => Effect.Effect<R, E>,
310+ cb: (
311+ trx: Parameters<DBAdapter["transaction"]>[0] extends (t: infer T) => unknown ? T : never,
312+ ) => Effect.Effect<R, E>,
336313 ) =>
337314 Effect.gen(function* () {
338315 const snapshot = cloneStore(store);
@@ -353,9 +330,7 @@ export const makeMemoryAdapter = ( · 14 unmodified lines
353330 supportsDates: true,
354331 supportsBooleans: true,
355332 supportsArrays: true,
356− customIdGenerator: options.generateId
357− ? () => options.generateId!()
358− : undefined,
333+ customIdGenerator: options.generateId ? () => options.generateId!() : undefined,
359334 transaction: txFn,
360335 },
361336 adapter: custom,