diff --git a/packages/io/src/lib.ts b/packages/io/src/lib.ts index 479a20d..304bcfa 100644 --- a/packages/io/src/lib.ts +++ b/packages/io/src/lib.ts @@ -3,6 +3,7 @@ import initSqlJs from 'sql.js' import * as schema from './schema' import { migrate } from './migrator' +import { SchedulesRepository, SchedulesRepositoryFacade } from './repositories' import { TagsRepository } from './repositories/tags' import { TasksRepository } from './repositories/tasks' @@ -17,6 +18,8 @@ export type StitchesIOConfig = { export interface StitchesIORepos { tasks: TasksRepository tags: TagsRepository + schedules: SchedulesRepository + schedulesFacade: SchedulesRepositoryFacade } export interface StitchesIOPort { @@ -79,10 +82,10 @@ function getFileLocator(url?: URL) { */ export async function open( database: Uint8Array, - config: StitchesIOConfig = {} + config: StitchesIOConfig = {}, ): Promise { const cfg: initSqlJs.SqlJsConfig = { - locateFile: config.wasm !== false ? getFileLocator(config.wasm) : undefined + locateFile: config.wasm !== false ? getFileLocator(config.wasm) : undefined, } const SQLite = await initSqlJs(cfg) @@ -90,12 +93,14 @@ export async function open( const mapper = drizzle(sqlite, { casing: 'snake_case', schema: schema, - logger: config.log + logger: config.log, }) const repo: StitchesIORepos = { tasks: new TasksRepository(mapper), - tags: new TagsRepository(mapper) + tags: new TagsRepository(mapper), + schedules: new SchedulesRepository(mapper), + schedulesFacade: new SchedulesRepositoryFacade(mapper), } const port: StitchesIOPort = { @@ -105,7 +110,7 @@ export async function open( migrate: () => migrate(mapper), export: () => sqlite.export(), clone: async (database: Uint8Array) => await open(database, config), - close: () => sqlite.close() + close: () => sqlite.close(), } return port diff --git a/packages/io/src/repositories/criteria.builder.ts b/packages/io/src/repositories/criteria.builder.ts new file mode 100644 index 0000000..a3f8cc7 --- /dev/null +++ b/packages/io/src/repositories/criteria.builder.ts @@ -0,0 +1,251 @@ +import { never, plural } from '@stitches/common' + +import { + ExtractTablesWithRelations, + InferColumnsDataTypes, + SQL, + SQLWrapper, + and, + between, + eq, + gt, + gte, + inArray, + isNotNull, + isNull, + like, + lt, + lte, + ne, + not, + notBetween, + notInArray, + notLike, + or, +} from 'drizzle-orm' + +import * as schema from '../schema' +import { TCols, TKeys } from './factory' +import { Table } from './utils' + +export enum Logical { + AND = 'AND', + OR = 'OR', + NOT = 'NOT', +} + +export enum Op { + EQ = 'EQ', + NE = 'NE', + GT = 'GT', + LT = 'LT', + IN = 'IN', + NIN = 'NIN', + GTE = 'GTE', + LTE = 'LTE', + NULL = 'NULL', + N_NULL = 'N_NULL', + LIKE = 'LIKE', + N_LIKE = 'N_LIKE', + BETWEEN = 'BETWEEN', + N_BETWEEN = 'N_BETWEEN', +} + +type Fields< + S extends schema.Schema, + K extends TKeys, +> = keyof ExtractTablesWithRelations[K]['columns'] + +type KeyForT>>, S extends schema.Schema> = { + [P in TKeys]: S[P] extends T ? P : never +}[TKeys] + +type FieldType< + F extends Fields>, + T extends Table>>, + S extends schema.Schema, +> = InferColumnsDataTypes[KeyForT]['columns']>[F] + +type UnaryOp = Op.NULL | Op.N_NULL +type BinaryOp = Op.EQ | Op.GT | Op.GTE | Op.LIKE | Op.N_LIKE | Op.LT | Op.LTE | Op.NE +type TernaryOp = Op.BETWEEN | Op.N_BETWEEN +type N_AryOp = Op.IN | Op.NIN + +type RHS = ({ [P in UnaryOp]: undefined } & { [P in BinaryOp]: T } & { + [P in TernaryOp]: [T, T] +} & { [P in N_AryOp]: T[] })[O] + +export interface Criteria { + unwrap(): SQL | undefined +} + +export interface CriteriaBuilder>>> + extends _CriteriaBuilder {} + +export function getCriteriaBuilder>>>( + table: T, +): CriteriaBuilder { + return new _CriteriaBuilder(table) +} + +class _Criteria implements Criteria { + constructor(private readonly criteria: SQL | undefined) {} + unwrap(): SQL | undefined { + return this.criteria + } +} + +class _CriteriaBuilder>>> { + private readonly criteria: SQL[] + private readonly AND: SQLWrapper[] + private readonly OR: SQLWrapper[] + private readonly NOT: SQLWrapper[] + + private readonly logicals: Logical[] + /** Stack pointer for both `AND`, `OR`, and `NOT` logicals */ + private readonly sp: { [K in keyof typeof Logical]: number[] } + + constructor(private readonly table: T) { + this.criteria = [] + + this.AND = [] + this.OR = [] + this.NOT = [] + + this.logicals = [] + + this.sp = { AND: [], OR: [], NOT: [] } + } + + private cleanup() { + this.criteria.length = this.logicals.length = 0 + this.AND.length = this.OR.length = this.NOT.length = 0 + this.sp.AND.length = this.sp.OR.length = this.sp.NOT.length = 0 + } + + private evaluate< + F extends Fields>, + O extends Op, + V extends RHS, O>, + >(lhs: F, op: O, rhs: V) { + const t = (_: O, v: typeof rhs): RHS, O> => v as any + const _op = op as Op + + switch (_op) { + case Op.BETWEEN: + return between(this.table[lhs], ...t(_op, rhs)) + case Op.IN: + return inArray(this.table[lhs], t(_op, rhs)) + case Op.EQ: + return eq(this.table[lhs], t(_op, rhs)) + case Op.GT: + return gt(this.table[lhs], t(_op, rhs)) + case Op.GTE: + return gte(this.table[lhs], t(_op, rhs)) + case Op.LIKE: + return like(this.table[lhs], t(_op, rhs)!) + case Op.LT: + return lt(this.table[lhs], t(_op, rhs)) + case Op.LTE: + return lte(this.table[lhs], t(_op, rhs)) + case Op.NE: + return ne(this.table[lhs], t(_op, rhs)) + case Op.NULL: + return isNull(this.table[lhs]) + case Op.NIN: + return notInArray(this.table[lhs], t(_op, rhs)) + case Op.N_BETWEEN: + return notBetween(this.table[lhs], ...t(_op, rhs)) + case Op.N_LIKE: + return notLike(this.table[lhs], t(_op, rhs)!) + case Op.N_NULL: + return isNotNull(this.table[lhs]) + default: + never(_op) + } + } + + private peek(stack: Array) { + return stack.at(-1) + } + + push(l: Logical) { + this.logicals.push(l) + this.sp[l].push(this[l].length) + return this + } + + pop() { + const logical = this.logicals.pop() + + if (logical === undefined) return this + + const handler = (cond: SQL) => { + const parent = this.peek(this.logicals) + if (parent) { + this[parent].push(cond) + } else { + this.criteria.push(cond) + } + } + + switch (logical) { + case Logical.AND: + handler(and(...this.AND.splice(this.sp.AND.pop()!, this.AND.length))!) + break + case Logical.OR: + handler(or(...this.OR.splice(this.sp.OR.pop()!, this.OR.length))!) + break + case Logical.NOT: + handler(not(this.NOT.splice(this.sp.NOT.pop()!, this.NOT.length).at(0)!)) + break + default: + never(logical) + } + + return this + } + + and(pipe: (cb: CriteriaBuilder) => CriteriaBuilder) { + return pipe(this.push(Logical.AND)).pop() + } + + or(pipe: (cb: CriteriaBuilder) => CriteriaBuilder) { + return pipe(this.push(Logical.OR)).pop() + } + + not(pipe: (cb: CriteriaBuilder) => CriteriaBuilder) { + return pipe(this.push(Logical.NOT)).pop() + } + + on>, O extends Op>(f: F, op: O, v: RHS, O>) { + const logical = this.peek(this.logicals) + + if (logical === undefined) { + this.criteria.push(this.evaluate(f, op, v)) + return this + } + + this[logical].push(this.evaluate(f, op, v)) + return this + } + + build(): Criteria { + const logicCount = this.logicals.length + if (logicCount > 0) { + throw new Error( + `Cannot build with ${logicCount} active ${plural(logicCount, 'logical', 'logicals')} on the stack`, + ) + } + + if (this.criteria.length > 1) { + const criteria = new _Criteria(and(...this.criteria)) + this.cleanup() + return criteria + } + + const criteria = new _Criteria(this.criteria.at(0)) + this.cleanup() + return criteria + } +} diff --git a/packages/io/src/repositories/factory.ts b/packages/io/src/repositories/factory.ts index d5ea18c..0ac993c 100644 --- a/packages/io/src/repositories/factory.ts +++ b/packages/io/src/repositories/factory.ts @@ -13,6 +13,7 @@ import { SQLJsDatabase } from 'drizzle-orm/sql-js' import * as schema from '../schema' import { fragments } from '../utils' +import { Criteria, getCriteriaBuilder } from './criteria.builder' import { BaseColumns, Table, withRedacted, withUnredacted } from './utils' export enum CollectionErrno { @@ -35,7 +36,10 @@ export class CollectionError extends Error { /** * Compute all the table keys from the given schema */ -export type TKeys = keyof ExtractTablesWithRelations +export type TKeys = Exclude< + keyof ExtractTablesWithRelations, + 'tagsToTasks' +> /** * Retrieve the columns of a table given by `S[K]` and ensure that the columns conform @@ -115,9 +119,15 @@ export function RepositoryAbstractFactory< * @param session The database session to use to create a new repository * @returns A new repository created with the given database session */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - withSession(session: SQLJsDatabase): AbstractRepository { - throw new Error('Unimplemented method') + abstract withSession(session: SQLJsDatabase): AbstractRepository + + /** + * Get the criteria builder specific to the repository + * @returns The criteria builder for the repository + */ + getCriteriaBuilder() { + // @ts-expect-error + return getCriteriaBuilder(options.table) } /** @@ -201,12 +211,12 @@ export function RepositoryAbstractFactory< * * @returns A list of `Entities` */ - async findMany() { + async findMany(criteria?: Criteria) { const filters = withUnredacted(options.table, []) const entity = await this.db .select() .from(options.table) - .where(and(...filters)) + .where(and(criteria?.unwrap(), ...filters)) return entity } diff --git a/packages/io/src/repositories/index.ts b/packages/io/src/repositories/index.ts index 3fe1316..6f8183a 100644 --- a/packages/io/src/repositories/index.ts +++ b/packages/io/src/repositories/index.ts @@ -1 +1,5 @@ +export * from './associations' +export * from './criteria.builder' +export * from './schedules' +export * from './tags' export * from './tasks' diff --git a/packages/io/src/repositories/schedules.ts b/packages/io/src/repositories/schedules.ts index ca277e2..9868519 100644 --- a/packages/io/src/repositories/schedules.ts +++ b/packages/io/src/repositories/schedules.ts @@ -19,6 +19,30 @@ export namespace record { export type RegFreqYearlyExprs = typeof schema.regularFrequencyYearlyExprs.$inferSelect } +export namespace api { + export type RegFreqYearlyExprs = ReturnType +} + +function getRegFreqYearlyExprs(exprs: record.RegFreqYearlyExprs) { + const { ordinal, variableWeekday, constantWeekday, createdAt, updatedAt, deletedAt, ...rest } = + exprs + return { + ...rest, + get on() { + return ordinal === null + ? undefined + : { + ordinal, + constantWeekday: constantWeekday!, + variableWeekday: variableWeekday!, + } + }, + createdAt, + updatedAt, + deletedAt, + } +} + /** * Schedules repository for working with, and managing, schedules */ @@ -154,23 +178,10 @@ export class SchedulesRepository extends RepositoryAbstractFactory('schedules', } case 'year': { - const { ordinal, variableWeekday, constantWeekday, ...rest } = - result.regular_frequency_yearly_exprs! return { ...result.regular_frequencies, type: result.regular_frequencies.type, - exprs: { - ...rest, - get on() { - return ordinal === null - ? undefined - : { - ordinal, - constantWeekday: constantWeekday!, - variableWeekday: variableWeekday!, - } - }, - }, + exprs: getRegFreqYearlyExprs(result.regular_frequency_yearly_exprs!), } } @@ -389,6 +400,7 @@ export class SchedulesRepositoryFacade { const result = await this.withTransaction(async ($this) => { if (payload.frequency.type === 'never') { const schedule = await $this.schedules.createOne({ + id: payload.id, taskId: payload.taskId, timestamp: payload.timestamp!, anchorTimestamp: payload.timestamp!, @@ -398,6 +410,7 @@ export class SchedulesRepositoryFacade { } const schedule = await $this.schedules.createOne({ + id: payload.id, taskId: payload.taskId, timestamp: payload.timestamp!, anchorTimestamp: payload.timestamp!, @@ -527,7 +540,7 @@ export class SchedulesRepositoryFacade { frequency: { ...regularFrequency, type: payload.frequency.type, - exprs: regFreqYearlyExprs, + exprs: getRegFreqYearlyExprs(regFreqYearlyExprs), }, } } diff --git a/packages/io/src/repositories/tags.ts b/packages/io/src/repositories/tags.ts index db1fd5c..a243d30 100644 --- a/packages/io/src/repositories/tags.ts +++ b/packages/io/src/repositories/tags.ts @@ -20,4 +20,8 @@ export class TagsRepository extends RepositoryAbstractFactory('tags', { table: s super(db) this.tasks = new TagsToTaskAssociation(db) } + + withSession(session: SQLJsDatabase): TagsRepository { + return new TagsRepository(session) + } } diff --git a/packages/io/src/repositories/tasks.ts b/packages/io/src/repositories/tasks.ts index 4368dc7..b26f4f6 100644 --- a/packages/io/src/repositories/tasks.ts +++ b/packages/io/src/repositories/tasks.ts @@ -19,4 +19,8 @@ export class TasksRepository extends RepositoryAbstractFactory('tasks', { table: super(db) this.tags = new TagsToTaskAssociation(db) } + + withSession(session: SQLJsDatabase): TasksRepository { + return new TasksRepository(session) + } } diff --git a/packages/io/tests/repository/criteria.builder.test.ts b/packages/io/tests/repository/criteria.builder.test.ts new file mode 100644 index 0000000..c7b7664 --- /dev/null +++ b/packages/io/tests/repository/criteria.builder.test.ts @@ -0,0 +1,80 @@ +import { beforeAll, beforeEach, describe, expect, it } from 'vitest' + +import * as schema from '../../src/schema' +import { StitchesIOPort, open } from '../../src/lib' +import { CriteriaBuilder, Logical, Op, getCriteriaBuilder } from '../../src/repositories' + +describe('#CriteriaBuilder', () => { + let port: StitchesIOPort + let cb: CriteriaBuilder + + const database = new Uint8Array() + + beforeAll(async () => { + port = await open(database, { wasm: false, log: false }).then((port) => (port.migrate(), port)) + }) + + beforeEach(() => { + cb = getCriteriaBuilder(schema.tasks) + }) + + it('should build a criteria using explicit stack management api', () => { + // prettier-ignore + const criteria = cb + .push(Logical.AND) + .on('title', Op.EQ, 'Say My Name') + .on('summary', Op.NE, "I'm game") + .push(Logical.OR) + .push(Logical.NOT) + .on('createdAt', Op.BETWEEN, [new Date('2023-10-25'), new Date('2024-10-25')]) + .pop() + .on('id', Op.IN, ['1', '2', '3']) + .pop() + .pop() + .build() + + const query = port.mapper + .select({ id: schema.tasks.id }) + .from(schema.tasks) + .where(criteria.unwrap()) + const sql = query.toSQL() + + expect(JSON.stringify(sql.params)).toMatchInlineSnapshot( + `"["Say My Name","I'm game",1698192000000,1729814400000,"1","2","3"]"`, + ) + expect(sql.sql).toMatchInlineSnapshot( + `"select "id" from "tasks" where ("tasks"."title" = ? and "tasks"."summary" <> ? and (not "tasks"."created_at" between ? and ? or "tasks"."id" in (?, ?, ?)))"`, + ) + }) + + it('should build a criteria using a more idiomatic api', () => { + const criteria = cb + .and((and) => { + return and + .on('title', Op.EQ, 'Say My Name') + .on('summary', Op.NE, "I'm game") + .or((or) => { + return or + .not((not) => { + return not.on('createdAt', Op.BETWEEN, [ + new Date('2023-10-25'), + new Date('2024-10-25'), + ]) + }) + .on('id', Op.IN, ['1', '2', '3']) + }) + }) + .build() + + const query = port.mapper + .select({ id: schema.tasks.id }) + .from(schema.tasks) + .where(criteria.unwrap()?.inlineParams()) + const sql = query.toSQL() + + expect(JSON.stringify(sql.params)).toMatchInlineSnapshot(`"[]"`) + expect(sql.sql).toMatchInlineSnapshot( + `"select "id" from "tasks" where ("tasks"."title" = 'Say My Name' and "tasks"."summary" <> 'I''m game' and (not "tasks"."created_at" between 1698192000000 and 1729814400000 or "tasks"."id" in ('1', '2', '3')))"`, + ) + }) +}) diff --git a/packages/io/tests/repository/schedules.test.ts b/packages/io/tests/repository/schedules.test.ts index 45b48d9..4f3d216 100644 --- a/packages/io/tests/repository/schedules.test.ts +++ b/packages/io/tests/repository/schedules.test.ts @@ -1,27 +1,25 @@ -import { sql } from 'drizzle-orm' -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest' +import { TaskSchedule } from '@stitches/common' + +import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { StitchesIOPort, open } from '../../src/lib' -import { SchedulesRepository } from '../../src/repositories/schedules' +import { Op, getCriteriaBuilder } from '../../src/repositories' +import { SchedulesRepository, SchedulesRepositoryFacade } from '../../src/repositories/schedules' import { TaskCreatePayload, TasksRepository } from '../../src/repositories/tasks' describe('#SchedulesRepository', () => { let port: StitchesIOPort - let schedulesRepository: SchedulesRepository let tasksRepository: TasksRepository + let schedulesRepository: SchedulesRepository const seedSize = 1000 // 10922: after which `Error: too many SQL variables` const database = new Uint8Array() type ModelCreate = typeof port.schema.schedules.$inferInsert - beforeAll(async () => { + beforeEach(async () => { port = await open(database, { wasm: false, log: false }).then((port) => (port.migrate(), port)) - }) - afterAll(() => port.close()) - - beforeEach(async () => { schedulesRepository = new SchedulesRepository(port.mapper) tasksRepository = new TasksRepository(port.mapper) @@ -57,12 +55,7 @@ describe('#SchedulesRepository', () => { await schedulesRepository.create([...regularsPayload, ...customsPayload]) }) - afterEach(() => { - port.mapper.run(sql` - DELETE FROM ${port.schema.tasks}; -- THIS SHOULD CASCADE - DELETE FROM ${port.schema.schedules}; -- BUT LET'S GO AHEAD AND STILL DELETE - `) - }) + afterEach(() => port.close()) it('should construct a new `SchedulesRepository` object', () => { expect(schedulesRepository).toBeDefined() @@ -110,3 +103,259 @@ describe('#SchedulesRepository', () => { }) }) }) + +describe('#SchedulesRepositoryFacade', () => { + let port: StitchesIOPort + let tasksRepository: TasksRepository + let schedulesRepository: SchedulesRepository + let schedulesRepositoryFacade: SchedulesRepositoryFacade + + const database = new Uint8Array() + const seedSize = 500 + + const date = new Date('2025-11-27T21:19:00.319Z') + const until = new Date('2025-11-18') + + const taskSchedules: Array = [ + { + id: '1', + taskId: '1', + timestamp: date, + frequency: { + type: 'custom', + crons: [{ expression: '*/2 * * * *', frequency: 'hour' }], + until: until, + }, + }, + { + id: '2', + taskId: '2', + timestamp: date, + frequency: { + type: 'year', + until: until, + exprs: { + every: 5, + subexpr: { + in: { + months: [0, 5, 11], // Jan, Jun, Dec + }, + on: { + ordinal: 'fourth', + weekday: 6, // Sat + }, + }, + }, + }, + }, + { + id: '3', + taskId: '3', + timestamp: date, + frequency: { + type: 'year', + until: until, + exprs: { + every: 2, + subexpr: { + in: { + months: [2, 5, 8, 11], // Mar, Jun, Sep, Dec + }, + on: { ordinal: 'last', variable: 'weekend-day' }, + }, + }, + }, + }, + { + id: '4', + taskId: '4', + timestamp: date, + frequency: { + type: 'month', + until: undefined, + exprs: { + every: 2, + subexpr: { + type: 'ondays', + days: [1, 10, 20, 30], + }, + }, + }, + }, + { + id: '5', + taskId: '5', + timestamp: date, + frequency: { + type: 'month', + until: undefined, + exprs: { + every: 1, + subexpr: { + type: 'onthe', + ordinal: 'fifth', + weekday: 4, // Thu + }, + }, + }, + }, + ] + + beforeEach(async () => { + port = await open(database, { wasm: false, log: false }).then((port) => (port.migrate(), port)) + tasksRepository = new TasksRepository(port.mapper) + schedulesRepository = new SchedulesRepository(port.mapper) + schedulesRepositoryFacade = new SchedulesRepositoryFacade(port.mapper) + + const payloads: TaskCreatePayload[] = Array.from({ length: seedSize }).map((_, i) => ({ + id: `${i + 1}`, + title: `Task ${i + 1}`, + summary: `This makes ${i + 1} task(s)`, + })) + + await tasksRepository.create(payloads) + }) + + afterEach(() => port.close()) + + it('should add scheudles to the repository', async () => { + const schedules = await Promise.all(taskSchedules.map((s) => schedulesRepositoryFacade.add(s))) + const criteria = getCriteriaBuilder(port.schema.schedules) + .or((or) => + or + .on('frequencyType', Op.EQ, 'custom') + .on('id', Op.IN, ['1', '2', '3']) + .on('deletedAt', Op.N_NULL, undefined), + ) + + .build() + + // const query = port.mapper + // .select({ id: port.schema.schedules.id }) + // .from(port.schema.schedules) + // .where(criteria.unwrap()) + // const sql = query.toSQL() + + const schedulex = await schedulesRepository.findMany(criteria) + + expect(schedules.at(0)).toMatchObject({ + id: '1', + taskId: '1', + anchorTimestamp: date, + timestamp: date, + frequencyType: 'custom', + until: until, + deletedAt: null, + frequency: [ + { + scheduleId: '1', + type: null, + expression: '*/2 * * * *', + deletedAt: null, + }, + ], + }) + + expect(schedules.at(1)).toMatchObject({ + id: '2', + taskId: '2', + anchorTimestamp: date, + timestamp: date, + frequencyType: 'year', + until: until, + deletedAt: null, + frequency: { + scheduleId: '2', + type: 'year', + every: 5, + deletedAt: null, + exprs: { + months: 2081, + deletedAt: null, + on: { + ordinal: 'fourth', + constantWeekday: 6, + variableWeekday: null, + }, + }, + }, + }) + + expect(schedules.at(2)).toMatchObject({ + id: '3', + taskId: '3', + anchorTimestamp: date, + timestamp: date, + frequencyType: 'year', + until: until, + deletedAt: null, + frequency: { + scheduleId: '3', + type: 'year', + every: 2, + deletedAt: null, + exprs: { + months: 2340, + deletedAt: null, + on: { + ordinal: 'last', + constantWeekday: null, + variableWeekday: 'weekend-day', + }, + }, + }, + }) + expect(schedules.at(3)).toMatchObject({ + id: '4', + taskId: '4', + anchorTimestamp: date, + timestamp: date, + frequencyType: 'month', + until: null, + deletedAt: null, + frequency: { + scheduleId: '4', + type: 'month', + every: 2, + deletedAt: null, + exprs: { + type: 'ondays', + deletedAt: null, + subexpr: { + days: 1074791426, + deletedAt: null, + }, + }, + }, + }) + + expect(schedules.at(4)).toMatchObject({ + id: '5', + taskId: '5', + anchorTimestamp: date, + timestamp: date, + frequencyType: 'month', + until: null, + deletedAt: null, + frequency: { + scheduleId: '5', + type: 'month', + every: 1, + deletedAt: null, + exprs: { + type: 'onthe', + deletedAt: null, + subexpr: { + ordinal: 'fifth', + weekday: 4, + deletedAt: null, + }, + }, + }, + }) + + console.log('%o', schedulex) + }) + + it.todo('should load schedules from the repository with all relevant relations', async () => {}) +}) diff --git a/packages/io/tests/repository/tags.test.ts b/packages/io/tests/repository/tags.test.ts index 28e5d41..94a6e4d 100644 --- a/packages/io/tests/repository/tags.test.ts +++ b/packages/io/tests/repository/tags.test.ts @@ -1,5 +1,4 @@ -import { sql } from 'drizzle-orm' -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { StitchesIOPort, open } from '../../src/lib' import { CollectionError } from '../../src/repositories/factory' @@ -12,13 +11,9 @@ describe('#TagsRepository', () => { const seedSize = 1000 // 10922: after which `Error: too many SQL variables` const database = new Uint8Array() - beforeAll(async () => { + beforeEach(async () => { port = await open(database, { wasm: false, log: false }).then((port) => (port.migrate(), port)) - }) - - afterAll(() => port.close()) - beforeEach(async () => { tagsRepository = new TagsRepository(port.mapper) const payloads: TagsCreatePayload[] = Array.from({ length: seedSize }).map((_, i) => ({ @@ -29,27 +24,23 @@ describe('#TagsRepository', () => { await tagsRepository.create(payloads) }) - afterEach(() => { - port.mapper.run(sql` - DELETE FROM ${port.schema.tags}; - `) - }) + afterEach(() => port.close()) it('should construct a new `TagsRepository` object', () => { expect(new TagsRepository(port.mapper)).toBeDefined() }) - // describe('#withSession', () => { - // it('should create a new repository with the given session', () => { - // port.mapper.transaction((tx) => { - // const newTagsRepository = tagsRepository.withSession(tx) - - // expect(newTagsRepository).toBeInstanceOf(TagsRepository) - // expect(newTagsRepository.db).toStrictEqual(tx) - // expect(tagsRepository.db).not.toStrictEqual(tx) - // }) - // }) - // }) + describe('#withSession', () => { + it('should create a new repository with the given session', () => { + port.mapper.transaction((tx) => { + const newTagsRepository = tagsRepository.withSession(tx) + + expect(newTagsRepository).toBeInstanceOf(TagsRepository) + expect(newTagsRepository.db).toStrictEqual(tx) + expect(tagsRepository.db).not.toStrictEqual(tx) + }) + }) + }) describe('#findMany', () => { it('should find as many tags', async () => { diff --git a/packages/io/tests/repository/tasks.test.ts b/packages/io/tests/repository/tasks.test.ts index 73b517c..a8c7c86 100644 --- a/packages/io/tests/repository/tasks.test.ts +++ b/packages/io/tests/repository/tasks.test.ts @@ -1,5 +1,4 @@ -import { sql } from 'drizzle-orm' -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { StitchesIOPort, open } from '../../src/lib' import { CollectionError } from '../../src/repositories/factory' @@ -12,13 +11,9 @@ describe('#TaskRepository', () => { const seedSize = 1000 // 10922: after which `Error: too many SQL variables` const database = new Uint8Array() - beforeAll(async () => { + beforeEach(async () => { port = await open(database, { wasm: false, log: false }).then((port) => (port.migrate(), port)) - }) - - afterAll(() => port.close()) - beforeEach(async () => { tasksRepository = new TasksRepository(port.mapper) const payloads: TaskCreatePayload[] = Array.from({ length: seedSize }).map((_, i) => ({ @@ -30,27 +25,23 @@ describe('#TaskRepository', () => { await tasksRepository.create(payloads) }) - afterEach(async () => { - port.mapper.run(sql` - DELETE FROM ${port.schema.tasks}; - `) - }) + afterEach(() => port.close()) it('should construct a new `TaskRepository` object', () => { expect(new TasksRepository(port.mapper)).toBeDefined() }) - // describe('#withSession', () => { - // it('should create a new repository with the given session', () => { - // port.mapper.transaction((tx) => { - // const newTasksRepository = tasksRepository.withSession(tx) - - // expect(newTasksRepository).toBeInstanceOf(TasksRepository) - // expect(newTasksRepository.db).toStrictEqual(tx) - // expect(tasksRepository.db).not.toStrictEqual(tx) - // }) - // }) - // }) + describe('#withSession', () => { + it('should create a new repository with the given session', () => { + port.mapper.transaction((tx) => { + const newTasksRepository = tasksRepository.withSession(tx) + + expect(newTasksRepository).toBeInstanceOf(TasksRepository) + expect(newTasksRepository.db).toStrictEqual(tx) + expect(tasksRepository.db).not.toStrictEqual(tx) + }) + }) + }) describe('#findMany', () => { it('should find as many tasks', async () => { diff --git a/vitest.workspace.ts b/vitest.workspace.ts index d4fd90d..e126583 100644 --- a/vitest.workspace.ts +++ b/vitest.workspace.ts @@ -1,7 +1,5 @@ import { configDefaults, defineWorkspace } from 'vitest/config' -global.self ||= {} as typeof global.self - const workspace = defineWorkspace([ { extends: './vitest.config.ts'