diff --git a/apps/crawler/package.json b/apps/crawler/package.json deleted file mode 100644 index 3bfbb5e..0000000 --- a/apps/crawler/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "@basango/crawler", - "version": "0.1.0", - "private": true, - "type": "module", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "scripts": { - "build": "tsc -b", - "test": "vitest --run" - }, - "dependencies": { - "bullmq": "^4.17.0", - "date-fns": "^3.6.0", - "ioredis": "^5.3.2", - "tiktoken": "^1.0.14", - "zod": "^4.0.0" - } -} diff --git a/apps/crawler/src/config.test.ts b/apps/crawler/src/config.test.ts deleted file mode 100644 index 14a5ea3..0000000 --- a/apps/crawler/src/config.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; - -import { describe, expect, it } from "vitest"; - -import { loadConfig } from "./config"; -import { resolveConfigPath } from "./schema"; - -describe("loadConfig", () => { - it("parses json configuration and ensures directories", () => { - const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "crawler-config-")); - const paths = { - root: tempDir, - data: path.join(tempDir, "data"), - logs: path.join(tempDir, "logs"), - configs: path.join(tempDir, "configs"), - }; - - const configPath = path.join(tempDir, "pipeline.json"); - fs.writeFileSync( - configPath, - JSON.stringify( - { - paths, - fetch: { - client: { timeout: 10 }, - }, - }, - null, - 2, - ), - ); - - const config = loadConfig({ configPath }); - - expect(config.fetch.client.timeout).toBe(10); - expect(fs.existsSync(paths.data)).toBe(true); - expect(fs.existsSync(paths.logs)).toBe(true); - expect(fs.existsSync(paths.configs)).toBe(true); - }); - - it("merges environment override if available", () => { - const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "crawler-config-")); - const paths = { - root: tempDir, - data: path.join(tempDir, "data"), - logs: path.join(tempDir, "logs"), - configs: path.join(tempDir, "configs"), - }; - - const basePath = path.join(tempDir, "pipeline.json"); - fs.writeFileSync( - basePath, - JSON.stringify( - { - paths, - logging: { level: "INFO" }, - }, - null, - 2, - ), - ); - - const overridePath = resolveConfigPath(basePath, "production"); - fs.writeFileSync( - overridePath, - JSON.stringify( - { - logging: { level: "DEBUG" }, - }, - null, - 2, - ), - ); - - const config = loadConfig({ configPath: basePath, env: "production" }); - - expect(config.logging.level).toBe("DEBUG"); - }); -}); diff --git a/apps/crawler/src/config.ts b/apps/crawler/src/config.ts deleted file mode 100644 index 70df6df..0000000 --- a/apps/crawler/src/config.ts +++ /dev/null @@ -1,88 +0,0 @@ -import fs from "node:fs"; -import path from "node:path"; - -import { - PipelineConfig, - PipelineConfigSchema, - mergePipelineConfig, - resolveConfigPath, - resolveProjectPaths, -} from "./schema"; -import { ensureDirectories } from "./utils"; - -export interface LoadConfigOptions { - configPath?: string; - env?: string; -} - -const DEFAULT_CONFIG_FILES = [ - path.join(process.cwd(), "config", "pipeline.json"), - path.join(process.cwd(), "pipeline.json"), -]; - -const readJsonFile = (filePath: string): unknown => { - const contents = fs.readFileSync(filePath, "utf-8"); - return contents.trim() === "" ? {} : JSON.parse(contents); -}; - -const locateConfigFile = (explicit?: string): string => { - if (explicit && fs.existsSync(explicit)) { - return explicit; - } - - for (const candidate of DEFAULT_CONFIG_FILES) { - if (fs.existsSync(candidate)) { - return candidate; - } - } - - return DEFAULT_CONFIG_FILES[0]; -}; - -const readPipelineConfig = (configPath: string): PipelineConfig => { - if (!fs.existsSync(configPath)) { - return PipelineConfigSchema.parse({ - paths: resolveProjectPaths(path.resolve(".")), - }); - } - - const raw = readJsonFile(configPath); - return PipelineConfigSchema.parse(raw); -}; - -const applyEnvironmentOverride = ( - baseConfig: PipelineConfig, - basePath: string, - env?: string, -): PipelineConfig => { - if (!env || env === "development") { - return baseConfig; - } - - const overridePath = resolveConfigPath(basePath, env); - if (!fs.existsSync(overridePath)) { - return baseConfig; - } - - const overrides = PipelineConfigSchema.parse(readJsonFile(overridePath)); - return mergePipelineConfig(baseConfig, overrides); -}; - -export const loadConfig = (options: LoadConfigOptions = {}): PipelineConfig => { - const basePath = locateConfigFile(options.configPath); - const config = applyEnvironmentOverride( - readPipelineConfig(basePath), - basePath, - options.env, - ); - - ensureDirectories(config.paths); - return config; -}; - -export const dumpConfig = (config: PipelineConfig, targetPath?: string): void => { - const destination = targetPath ?? locateConfigFile(); - const normalized = PipelineConfigSchema.parse(config); - fs.mkdirSync(path.dirname(destination), { recursive: true }); - fs.writeFileSync(destination, JSON.stringify(normalized, null, 2)); -}; diff --git a/apps/crawler/src/schema.test.ts b/apps/crawler/src/schema.test.ts deleted file mode 100644 index 3b86eb3..0000000 --- a/apps/crawler/src/schema.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { - PipelineConfigSchema, - createDateRange, - formatDateRange, - isTimestampInRange, - PageRangeSpecSchema, - PageRangeSchema, - schemaToJSON, -} from "./schema"; - -describe("schema helpers", () => { - it("creates date range from spec", () => { - const range = createDateRange("2024-01-01:2024-01-31"); - expect(range.start).toBeLessThan(range.end); - expect(formatDateRange(range)).toBe("2024-01-01:2024-01-31"); - }); - - it("checks membership", () => { - const range = createDateRange("2024-01-01:2024-01-02"); - expect(isTimestampInRange(range, range.start)).toBe(true); - expect(isTimestampInRange(range, range.start - 1)).toBe(false); - }); - - it("parses page range spec", () => { - const range = PageRangeSchema.parse(PageRangeSpecSchema.parse("1:10")); - expect(range).toEqual({ start: 1, end: 10 }); - }); - - it("produces json schema", () => { - const json = schemaToJSON(PipelineConfigSchema); - expect(json.type).toBe("object"); - }); -}); diff --git a/apps/crawler/src/schema.ts b/apps/crawler/src/schema.ts deleted file mode 100644 index fdfa579..0000000 --- a/apps/crawler/src/schema.ts +++ /dev/null @@ -1,297 +0,0 @@ -import path from "node:path"; - -import { getUnixTime, parse, isMatch, format as formatDate } from "date-fns"; -import { z } from "zod"; - -export const UpdateDirectionSchema = z.enum(["forward", "backward"]); -export type UpdateDirection = z.infer; - -export const SourceKindSchema = z.enum(["wordpress", "html"]); -export type SourceKind = z.infer; - -export const SourceDateSchema = z.object({ - format: z.string().default("yyyy-LL-dd HH:mm"), - pattern: z.string().nullable().optional(), - replacement: z.string().nullable().optional(), -}); -export type SourceDate = z.infer; - -export const SourceSelectorsSchema = z.object({ - articles: z.string().optional().nullable(), - article_title: z.string().optional().nullable(), - article_link: z.string().optional().nullable(), - article_body: z.string().optional().nullable(), - article_date: z.string().optional().nullable(), - article_categories: z.string().optional().nullable(), - pagination: z.string().default("ul.pagination > li a"), -}); -export type SourceSelectors = z.infer; - -const BaseSourceSchema = z.object({ - source_id: z.string(), - source_url: z.string().url(), - source_date: SourceDateSchema.default(SourceDateSchema.parse({})), - source_kind: SourceKindSchema, - categories: z.array(z.string()).default([]), - supports_categories: z.boolean().default(false), - requires_details: z.boolean().default(false), - requires_rate_limit: z.boolean().default(false), -}); - -export const HtmlSourceConfigSchema = BaseSourceSchema.extend({ - source_kind: z.literal("html"), - source_selectors: SourceSelectorsSchema.default(SourceSelectorsSchema.parse({})), - pagination_template: z.string(), -}); - -export const WordPressSourceConfigSchema = BaseSourceSchema.extend({ - source_kind: z.literal("wordpress"), - source_date: SourceDateSchema.default( - SourceDateSchema.parse({ format: "yyyy-LL-dd'T'HH:mm:ss" }), - ), -}); - -export type HtmlSourceConfig = z.infer; -export type WordPressSourceConfig = z.infer; -export type AnySourceConfig = HtmlSourceConfig | WordPressSourceConfig; - -export const DateRangeSchema = z - .object({ - start: z.number().int(), - end: z.number().int(), - }) - .superRefine((value, ctx) => { - if (value.start === 0 || value.end === 0) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Timestamp cannot be zero", - }); - } - if (value.end < value.start) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "End timestamp must be greater than or equal to start", - }); - } - }); - -export type DateRange = z.infer; - -export const PageRangeSchema = z - .object({ - start: z.number().int().min(0), - end: z.number().int().min(0), - }) - .superRefine((value, ctx) => { - if (value.end < value.start) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "End page must be greater than or equal to start page", - }); - } - }); - -export type PageRange = z.infer; - -export const PageRangeSpecSchema = z - .string() - .regex(/^[0-9]+:[0-9]+$/, "Invalid page range format. Use start:end") - .transform((spec) => { - const [startText, endText] = spec.split(":"); - return { - start: Number.parseInt(startText, 10), - end: Number.parseInt(endText, 10), - }; - }); - -const defaultDateFormat = "yyyy-LL-dd"; - -export const DateRangeSpecSchema = z - .string() - .regex(/.+:.+/, "Expected start:end format") - .transform((spec) => { - const [startRaw, endRaw] = spec.split(":"); - return { startRaw, endRaw }; - }); - -const parseDate = (value: string, format: string): Date => { - if (!isMatch(value, format)) { - throw new Error(`Invalid date '${value}' for format '${format}'`); - } - const parsed = parse(value, format, new Date()); - if (Number.isNaN(parsed.getTime())) { - throw new Error(`Invalid date '${value}' for format '${format}'`); - } - return parsed; -}; - -export interface CreateDateRangeOptions { - format?: string; - separator?: string; -} - -export const createDateRange = ( - spec: string, - options: CreateDateRangeOptions = {}, -): DateRange => { - const { format = defaultDateFormat, separator = ":" } = options; - if (!separator) { - throw new Error("Separator cannot be empty"); - } - - const normalized = spec.replace(separator, ":"); - const parsedSpec = DateRangeSpecSchema.parse(normalized); - - const startDate = parseDate(parsedSpec.startRaw, format); - const endDate = parseDate(parsedSpec.endRaw, format); - - const range = { - start: getUnixTime(startDate), - end: getUnixTime(endDate), - }; - - return DateRangeSchema.parse(range); -}; - -export const formatDateRange = (range: DateRange, fmt = defaultDateFormat): string => { - const start = formatDate(new Date(range.start * 1000), fmt); - const end = formatDate(new Date(range.end * 1000), fmt); - return `${start}:${end}`; -}; - -export const isTimestampInRange = (range: DateRange, timestamp: number): boolean => { - return range.start <= timestamp && timestamp <= range.end; -}; - -export const ProjectPathsSchema = z.object({ - root: z.string(), - data: z.string(), - logs: z.string(), - configs: z.string(), -}); -export type ProjectPaths = z.infer; - -export const resolveProjectPaths = (rootDir: string): ProjectPaths => { - return ProjectPathsSchema.parse({ - root: rootDir, - data: path.join(rootDir, "data", "dataset"), - logs: path.join(rootDir, "data", "logs"), - configs: path.join(rootDir, "config"), - }); -}; - -export const LoggingConfigSchema = z.object({ - level: z.string().default("INFO"), - format: z - .string() - .default("%(asctime)s - %(name)s - %(levelname)s - %(message)s"), - console_logging: z.boolean().default(true), - file_logging: z.boolean().default(false), - log_file: z.string().default("crawler.log"), - max_log_size: z.number().int().positive().default(10 * 1024 * 1024), - backup_count: z.number().int().nonnegative().default(5), -}); -export type LoggingConfig = z.infer; - -export const ClientConfigSchema = z.object({ - timeout: z.number().positive().default(20), - user_agent: z - .string() - .default("Basango/0.1 (+https://github.com/bernard-ng/basango)"), - follow_redirects: z.boolean().default(true), - verify_ssl: z.boolean().default(true), - rotate: z.boolean().default(true), - max_retries: z.number().int().nonnegative().default(3), - backoff_initial: z.number().nonnegative().default(1), - backoff_multiplier: z.number().positive().default(2), - backoff_max: z.number().nonnegative().default(30), - respect_retry_after: z.boolean().default(true), -}); - -export const CrawlerConfigSchema = z.object({ - source: z.union([HtmlSourceConfigSchema, WordPressSourceConfigSchema]).optional(), - page_range: PageRangeSchema.optional(), - date_range: DateRangeSchema.optional(), - category: z.string().optional(), - notify: z.boolean().default(false), - is_update: z.boolean().default(false), - use_multi_threading: z.boolean().default(false), - max_workers: z.number().int().positive().default(5), - direction: UpdateDirectionSchema.default("forward"), -}); - -export type ClientConfig = z.infer; -export type CrawlerConfig = z.infer & { - source?: AnySourceConfig; -}; - -export const FetchConfigSchema = z.object({ - client: ClientConfigSchema.default(ClientConfigSchema.parse({})), - crawler: CrawlerConfigSchema.default(CrawlerConfigSchema.parse({})), -}); -export type FetchConfig = z.infer; - -const SourcesConfigSchema = z.object({ - html: z.array(HtmlSourceConfigSchema).default([]), - wordpress: z.array(WordPressSourceConfigSchema).default([]), -}); - -export type SourcesConfig = z.infer & { - find: (sourceId: string) => AnySourceConfig | undefined; -}; - -export const createSourcesConfig = (input: unknown): SourcesConfig => { - const parsed = SourcesConfigSchema.parse(input); - const resolver = (sourceId: string) => - [...parsed.html, ...parsed.wordpress].find((source) => source.source_id === sourceId); - return Object.assign({ find: resolver }, parsed); -}; - -export const PipelineConfigSchema = z.object({ - paths: ProjectPathsSchema.default(resolveProjectPaths(process.cwd())), - logging: LoggingConfigSchema.default(LoggingConfigSchema.parse({})), - fetch: FetchConfigSchema.default(FetchConfigSchema.parse({})), - sources: z - .union([SourcesConfigSchema, z.undefined()]) - .transform((value) => createSourcesConfig(value ?? {})), -}); - -export type PipelineConfig = z.infer & { - sources: SourcesConfig; -}; - -export const mergePipelineConfig = ( - base: PipelineConfig, - overrides: Partial, -): PipelineConfig => { - const paths = overrides.paths ?? base.paths; - const logging = { ...base.logging, ...(overrides.logging ?? {}) }; - const fetch = { - client: { ...base.fetch.client, ...(overrides.fetch?.client ?? {}) }, - crawler: { ...base.fetch.crawler, ...(overrides.fetch?.crawler ?? {}) }, - }; - - const sources = createSourcesConfig({ - html: overrides.sources?.html ?? base.sources.html, - wordpress: overrides.sources?.wordpress ?? base.sources.wordpress, - }); - - return { - paths, - logging, - fetch, - sources, - }; -}; - -export const resolveConfigPath = (basePath: string, env?: string): string => { - if (!env || env === "development") { - return basePath; - } - - const ext = path.extname(basePath); - const withoutExt = basePath.slice(0, basePath.length - ext.length); - return `${withoutExt}.${env}${ext}`; -}; - -export const schemaToJSON = (schema: T) => schema.toJSON(); diff --git a/apps/crawler/src/services/crawler/async/queue.test.ts b/apps/crawler/src/services/crawler/async/queue.test.ts deleted file mode 100644 index 2d971c4..0000000 --- a/apps/crawler/src/services/crawler/async/queue.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { createQueueManager, createQueueSettings } from "./queue"; - -class InMemoryQueue { - public jobs: Array<{ name: string; data: unknown }> = []; - - async add(name: string, data: unknown) { - this.jobs.push({ name, data }); - return { id: `${name}-${this.jobs.length}` }; - } -} - -describe("createQueueManager", () => { - it("prefixes queue names", () => { - const manager = createQueueManager({ - settings: createQueueSettings({ prefix: "test" }), - queueFactory: (queueName) => { - expect(queueName).toBe("listing"); - return new InMemoryQueue(); - }, - connection: { - quit: async () => undefined, - } as any, - }); - - expect(manager.iterQueueNames()).toEqual([ - "test:listing", - "test:articles", - "test:processed", - ]); - }); - - it("enqueues listing job with validated payload", async () => { - const queue = new InMemoryQueue(); - const manager = createQueueManager({ - queueFactory: () => queue, - connection: { quit: async () => undefined } as any, - }); - - const job = await manager.enqueueListing({ - source_id: "radiookapi", - env: "test", - }); - - expect(job.id).toBe("collect_listing-1"); - expect(queue.jobs[0]).toEqual({ - name: "collect_listing", - data: { - source_id: "radiookapi", - env: "test", - page_range: undefined, - date_range: undefined, - category: undefined, - }, - }); - }); -}); diff --git a/apps/crawler/src/services/crawler/async/queue.ts b/apps/crawler/src/services/crawler/async/queue.ts deleted file mode 100644 index 35ed193..0000000 --- a/apps/crawler/src/services/crawler/async/queue.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { randomUUID } from "node:crypto"; - -import IORedis from "ioredis"; -import { Queue, JobsOptions, QueueOptions } from "bullmq"; -import { z } from "zod"; - -import { - ListingTaskPayload, - ArticleTaskPayload, - ProcessedTaskPayload, - ListingTaskPayloadSchema, - ArticleTaskPayloadSchema, - ProcessedTaskPayloadSchema, -} from "./schemas"; -import { parseRedisUrl } from "../../../utils"; - -const QueueSettingsSchema = z.object({ - redis_url: z.string().default(process.env.BASANGO_REDIS_URL ?? "redis://localhost:6379/0"), - prefix: z.string().default(process.env.BASANGO_QUEUE_PREFIX ?? "crawler"), - default_timeout: z.number().int().positive().default(Number(process.env.BASANGO_QUEUE_TIMEOUT ?? 600)), - result_ttl: z.number().int().nonnegative().default(Number(process.env.BASANGO_QUEUE_RESULT_TTL ?? 3600)), - failure_ttl: z.number().int().nonnegative().default(Number(process.env.BASANGO_QUEUE_FAILURE_TTL ?? 3600)), - listing_queue: z.string().default("listing"), - article_queue: z.string().default("articles"), - processed_queue: z.string().default("processed"), -}); - -export type QueueSettingsInput = z.input; -export type QueueSettings = z.output; - -export const createQueueSettings = (input?: QueueSettingsInput): QueueSettings => - QueueSettingsSchema.parse(input ?? {}); - -export interface QueueBackend { - add: (name: string, data: T, opts?: JobsOptions) => Promise<{ id: string }>; -} - -export type QueueFactory = ( - queueName: string, - settings: QueueSettings, - connection?: IORedis, -) => QueueBackend; - -const defaultQueueFactory: QueueFactory = (queueName, settings, connection) => { - const redisConnection = connection ?? new IORedis(settings.redis_url, parseRedisUrl(settings.redis_url)); - const options: QueueOptions = { - connection: redisConnection, - prefix: settings.prefix, - }; - - const queue = new Queue(queueName, options); - return { - add: async (name, data, opts) => { - const job = await queue.add(name, data, { - removeOnComplete: settings.result_ttl === 0 ? true : undefined, - removeOnFail: settings.failure_ttl === 0 ? true : undefined, - timeout: settings.default_timeout * 1000, - ...opts, - }); - return { id: job.id ?? randomUUID() }; - }, - }; -}; - -export interface CreateQueueManagerOptions { - settings?: QueueSettings | QueueSettingsInput; - queueFactory?: QueueFactory; - connection?: IORedis; -} - -export interface QueueManager { - readonly settings: QueueSettings; - readonly connection: IORedis; - enqueueListing: (payload: ListingTaskPayload) => Promise<{ id: string }>; - enqueueArticle: (payload: ArticleTaskPayload) => Promise<{ id: string }>; - enqueueProcessed: (payload: ProcessedTaskPayload) => Promise<{ id: string }>; - iterQueueNames: () => string[]; - queueName: (suffix: string) => string; - close: () => Promise; -} - -export const createQueueManager = (options: CreateQueueManagerOptions = {}): QueueManager => { - const settings = createQueueSettings(options.settings as QueueSettingsInput | undefined); - - const connection = options.connection ?? new IORedis(settings.redis_url, parseRedisUrl(settings.redis_url)); - const factory = options.queueFactory ?? defaultQueueFactory; - - const ensureQueue = (queueName: string) => factory(queueName, settings, connection); - - return { - settings, - connection, - enqueueListing: (payload) => { - const data = ListingTaskPayloadSchema.parse(payload); - const queue = ensureQueue(settings.listing_queue); - return queue.add("collect_listing", data); - }, - enqueueArticle: (payload) => { - const data = ArticleTaskPayloadSchema.parse(payload); - const queue = ensureQueue(settings.article_queue); - return queue.add("collect_article", data); - }, - enqueueProcessed: (payload) => { - const data = ProcessedTaskPayloadSchema.parse(payload); - const queue = ensureQueue(settings.processed_queue); - return queue.add("forward_for_processing", data); - }, - iterQueueNames: () => [ - `${settings.prefix}:${settings.listing_queue}`, - `${settings.prefix}:${settings.article_queue}`, - `${settings.prefix}:${settings.processed_queue}`, - ], - queueName: (suffix: string) => `${settings.prefix}:${suffix}`, - close: async () => { - await connection.quit(); - }, - }; -}; diff --git a/apps/crawler/src/services/crawler/async/schemas.ts b/apps/crawler/src/services/crawler/async/schemas.ts deleted file mode 100644 index bb13743..0000000 --- a/apps/crawler/src/services/crawler/async/schemas.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { z } from "zod"; - -import { AnySourceConfig, DateRangeSchema, PageRangeSchema } from "../../../schema"; - -export const ListingTaskPayloadSchema = z.object({ - source_id: z.string(), - env: z.string().default("development"), - page_range: z.string().optional().nullable(), - date_range: z.string().optional().nullable(), - category: z.string().optional().nullable(), -}); - -export type ListingTaskPayload = z.infer; - -export const ArticleTaskPayloadSchema = z.object({ - source_id: z.string(), - env: z.string().default("development"), - url: z.string().url(), - page: z.number().int().nonnegative().optional(), - page_range: PageRangeSchema.optional().nullable(), - date_range: DateRangeSchema.optional().nullable(), - category: z.string().optional().nullable(), -}); - -export type ArticleTaskPayload = z.infer; - -export const ProcessedTaskPayloadSchema = z.object({ - source_id: z.string(), - env: z.string().default("development"), - article: z.any(), -}); - -export type ProcessedTaskPayload = z.infer; - -export interface ListingContext { - source: AnySourceConfig; -} diff --git a/apps/crawler/src/services/crawler/async/tasks.test.ts b/apps/crawler/src/services/crawler/async/tasks.test.ts deleted file mode 100644 index 6ae237d..0000000 --- a/apps/crawler/src/services/crawler/async/tasks.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; - -import { scheduleAsyncCrawl, registerCrawlerTaskHandlers, collectListing } from "./tasks"; -import { QueueManager } from "./queue"; - -describe("Async tasks", () => { - it("schedules crawl with provided manager", async () => { - const enqueueListing = vi.fn().mockResolvedValue({ id: "job-1" }); - const manager = { - enqueueListing, - } as unknown as QueueManager; - - const jobId = await scheduleAsyncCrawl({ - sourceId: "radiookapi", - queueManager: manager, - }); - - expect(jobId).toBe("job-1"); - expect(enqueueListing).toHaveBeenCalledWith({ - source_id: "radiookapi", - env: "development", - page_range: undefined, - date_range: undefined, - category: undefined, - }); - }); - - it("delegates listing collection to registered handler", async () => { - const handler = vi.fn().mockResolvedValue(5); - registerCrawlerTaskHandlers({ collectListing: handler }); - - const count = await collectListing({ - source_id: "radiookapi", - env: "development", - }); - - expect(count).toBe(5); - expect(handler).toHaveBeenCalledWith({ - source_id: "radiookapi", - env: "development", - page_range: undefined, - date_range: undefined, - category: undefined, - }); - }); -}); diff --git a/apps/crawler/src/services/crawler/async/tasks.ts b/apps/crawler/src/services/crawler/async/tasks.ts deleted file mode 100644 index d81f571..0000000 --- a/apps/crawler/src/services/crawler/async/tasks.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { - ListingTaskPayloadSchema, - ArticleTaskPayloadSchema, - ProcessedTaskPayloadSchema, - ListingTaskPayload, - ArticleTaskPayload, - ProcessedTaskPayload, -} from "./schemas"; -import { - createQueueManager, - QueueManager, - QueueSettings, - QueueSettingsInput, -} from "./queue"; - -export interface CrawlerTaskHandlers { - collectListing: (payload: ListingTaskPayload) => Promise | number; - collectArticle: (payload: ArticleTaskPayload) => Promise | unknown; - forwardForProcessing: (payload: ProcessedTaskPayload) => Promise | unknown; -} - -const notImplemented = (name: keyof CrawlerTaskHandlers) => () => { - throw new Error(`Crawler task handler '${name}' is not implemented`); -}; - -let handlers: CrawlerTaskHandlers = { - collectListing: notImplemented("collectListing"), - collectArticle: notImplemented("collectArticle"), - forwardForProcessing: notImplemented("forwardForProcessing"), -}; - -export const registerCrawlerTaskHandlers = (overrides: Partial): void => { - handlers = { ...handlers, ...overrides }; -}; - -export interface ScheduleAsyncCrawlOptions { - sourceId: string; - env?: string; - pageRange?: string | null; - dateRange?: string | null; - category?: string | null; - settings?: QueueSettings | QueueSettingsInput; - queueManager?: QueueManager; -} - -export const scheduleAsyncCrawl = async ({ - sourceId, - env = "development", - pageRange, - dateRange, - category, - settings, - queueManager, -}: ScheduleAsyncCrawlOptions): Promise => { - const payload = ListingTaskPayloadSchema.parse({ - source_id: sourceId, - env, - page_range: pageRange ?? undefined, - date_range: dateRange ?? undefined, - category: category ?? undefined, - }); - - const manager = queueManager ?? createQueueManager({ settings }); - try { - const job = await manager.enqueueListing(payload); - return job.id; - } finally { - if (!queueManager) { - await manager.close(); - } - } -}; - -export const collectListing = async (payload: unknown): Promise => { - const data = ListingTaskPayloadSchema.parse(payload); - const result = await handlers.collectListing(data); - return typeof result === "number" ? result : 0; -}; - -export const collectArticle = async (payload: unknown): Promise => { - const data = ArticleTaskPayloadSchema.parse(payload); - return handlers.collectArticle(data); -}; - -export const forwardForProcessing = async (payload: unknown): Promise => { - const data = ProcessedTaskPayloadSchema.parse(payload); - return handlers.forwardForProcessing(data); -}; diff --git a/apps/crawler/src/services/crawler/async/worker.ts b/apps/crawler/src/services/crawler/async/worker.ts deleted file mode 100644 index adbc506..0000000 --- a/apps/crawler/src/services/crawler/async/worker.ts +++ /dev/null @@ -1,83 +0,0 @@ -import IORedis from "ioredis"; -import { Worker, QueueEvents } from "bullmq"; - -import { - createQueueManager, - QueueFactory, - QueueManager, - QueueSettings, - QueueSettingsInput, -} from "./queue"; -import { collectArticle, collectListing, forwardForProcessing } from "./tasks"; - -export interface WorkerOptions { - queueNames?: string[]; - settings?: QueueSettings | QueueSettingsInput; - connection?: IORedis; - queueFactory?: QueueFactory; - concurrency?: number; - onError?: (error: Error) => void; - queueManager?: QueueManager; -} - -export interface WorkerHandle { - readonly workers: Worker[]; - readonly events: QueueEvents[]; - close: () => Promise; -} - -export const startWorker = (options: WorkerOptions = {}): WorkerHandle => { - const manager = - options.queueManager ?? - createQueueManager({ - settings: options.settings, - connection: options.connection, - queueFactory: options.queueFactory, - }); - - const queueNames = options.queueNames ?? manager.iterQueueNames(); - const workers: Worker[] = []; - const events: QueueEvents[] = []; - - const connection = manager.connection; - - for (const queueName of queueNames) { - const worker = new Worker(queueName, async (job) => { - switch (job.name) { - case "collect_listing": - return collectListing(job.data); - case "collect_article": - return collectArticle(job.data); - case "forward_for_processing": - return forwardForProcessing(job.data); - default: - throw new Error(`Unknown job name: ${job.name}`); - } - }, { - connection, - concurrency: options.concurrency ?? 5, - }); - - if (options.onError) { - worker.on("failed", (_, err) => options.onError?.(err as Error)); - worker.on("error", (err) => options.onError?.(err as Error)); - } - - const queueEvents = new QueueEvents(queueName, { connection }); - - workers.push(worker); - events.push(queueEvents); - } - - return { - workers, - events, - close: async () => { - await Promise.all(workers.map((worker) => worker.close())); - await Promise.all(events.map((event) => event.close())); - if (!options.queueManager) { - await manager.close(); - } - }, - }; -}; diff --git a/apps/crawler/src/utils.ts b/apps/crawler/src/utils.ts deleted file mode 100644 index ada900a..0000000 --- a/apps/crawler/src/utils.ts +++ /dev/null @@ -1,38 +0,0 @@ -import fs from "node:fs"; - -import type { RedisOptions } from "ioredis"; -import { get_encoding } from "tiktoken"; - -import type { ProjectPaths } from "./schema"; - -export const ensureDirectories = (paths: ProjectPaths): void => { - for (const dir of [paths.data, paths.logs, paths.configs]) { - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - } - } -}; - -export const parseRedisUrl = (url: string): RedisOptions => { - if (!url.startsWith("redis://")) { - return {}; - } - const parsed = new URL(url); - return { - host: parsed.hostname, - port: Number(parsed.port || 6379), - password: parsed.password || undefined, - db: Number(parsed.pathname?.replace("/", "") || 0), - }; -}; - -export const countTokens = (text: string, encoding = "cl100k_base"): number => { - try { - const encoder = get_encoding(encoding); - const tokens = encoder.encode(text); - encoder.free(); - return tokens.length; - } catch { - return text.length; - } -}; diff --git a/apps/crawler/tsconfig.json b/apps/crawler/tsconfig.json deleted file mode 100644 index d3399c2..0000000 --- a/apps/crawler/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "dist" - }, - "include": ["src"], - "references": [] -} diff --git a/apps/crawler/vitest.config.ts b/apps/crawler/vitest.config.ts deleted file mode 100644 index 869d8d7..0000000 --- a/apps/crawler/vitest.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { - environment: "node", - globals: true, - include: ["src/**/*.test.ts"], - }, -}); diff --git a/basango/.gitignore b/basango/.gitignore index 96fab4f..4d8e1d0 100644 --- a/basango/.gitignore +++ b/basango/.gitignore @@ -1,38 +1,86 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. -# Dependencies +# dependencies node_modules .pnp .pnp.js -# Local env files -.env -.env.local -.env.development.local -.env.test.local -.env.production.local - -# Testing +# testing coverage -# Turbo -.turbo - -# Vercel -.vercel - -# Build Outputs +# next.js .next/ out/ +next-env.d.ts + +# expo +.expo/ +dist/ + +# production build -dist +# misc +.DS_Store +*.pem -# Debug +# debug npm-debug.log* yarn-debug.log* yarn-error.log* +.pnpm-debug.log* -# Misc +# local env files +.env +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo + +# turbo +.turbo + +# react-email +.react-email +packages/email/public +!packages/email/public/.gitkeep +!packages/email/.react-email/.gitkeep + +# Storybook +storybook-static +tailwind.css + +# Supabase +.branches +.temp + +# Trigger +.trigger + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea .DS_Store -*.pem +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/basango/README.md b/basango/README.md index 240569f..43da50f 100644 --- a/basango/README.md +++ b/basango/README.md @@ -1,135 +1,42 @@ -# Turborepo starter +# Basango: Towards a scalable and intelligent system for Congolese News curation -This Turborepo starter is maintained by the Turborepo core team. +[![backend audit](https://github.com/bernard-ng/basango/actions/workflows/backend_audit.yaml/badge.svg)](https://github.com/bernard-ng/basango/actions/workflows/backend_audit.yaml) +[![backend deploy](https://github.com/bernard-ng/basango/actions/workflows/backend_deploy.yaml/badge.svg)](https://github.com/bernard-ng/basango/actions/workflows/backend_deploy.yaml) +[![backend quality](https://github.com/bernard-ng/basango/actions/workflows/backend_quality.yaml/badge.svg)](https://github.com/bernard-ng/basango/actions/workflows/backend_quality.yaml) +[![backend tests](https://github.com/bernard-ng/basango/actions/workflows/backend_tests.yaml/badge.svg)](https://github.com/bernard-ng/basango/actions/workflows/backend_tests.yaml) +[![crawler audit](https://github.com/bernard-ng/basango/actions/workflows/crawler_audit.yml/badge.svg)](https://github.com/bernard-ng/basango/actions/workflows/crawler_audit.yml) +[![crawler quality](https://github.com/bernard-ng/basango/actions/workflows/crawler_quality.yml/badge.svg)](https://github.com/bernard-ng/basango/actions/workflows/crawler_quality.yml) +[![crawler tests](https://github.com/bernard-ng/basango/actions/workflows/crawler_tests.yml/badge.svg)](https://github.com/bernard-ng/basango/actions/workflows/crawler_tests.yml) +[![mobile quality](https://github.com/bernard-ng/basango/actions/workflows/mobile_quality.yaml/badge.svg)](https://github.com/bernard-ng/basango/actions/workflows/mobile_quality.yaml) -## Using this example +| Scope | Link | +|-------------------|-------------------------------------------| +| Crawler | [README.md](./projects/crawler/README.md) | +| Backend | [README.md](./projects/backend/README.md) | +| Mobile | [README.md](./projects/mobile/README.md) | -Run the following command: +--- +### Introduction -```sh -npx create-turbo@latest -``` +The **"Basango"** is a structured and scalable system of news articles sourced from major media outlets covering diverse aspects of the Democratic Republic of Congo (DRC). Designed for efficiency, this system enables the automated collection, processing, and organization of news stories spanning politics, economy, society, culture, environment, and international affairs. -## What's inside? +This system is built to support large-scale text analysis, making it a valuable resource for researchers, journalists, policymakers, and data scientists. It facilitates tasks such as sentiment analysis, trend detection, entity recognition, and language modeling, providing deep insights into the evolving socio-political and economic landscape of the DRC. -This Turborepo includes the following packages/apps: +To ensure quality and reliability, the dataset prioritizes reputable news sources while maintaining an adaptable framework for continuous expansion. However, users are encouraged to critically assess the content, as journalistic standards and perspectives may vary. -### Apps and Packages +### Sources -- `docs`: a [Next.js](https://nextjs.org/) app -- `web`: another [Next.js](https://nextjs.org/) app -- `@repo/ui`: a stub React component library shared by both `web` and `docs` applications -- `@repo/eslint-config`: `eslint` configurations (includes `eslint-config-next` and `eslint-config-prettier`) -- `@repo/typescript-config`: `tsconfig.json`s used throughout the monorepo +| Source | Articles | Link | +|----------------------|----------|--------------------------------------| +| radiookapi.net | +100k | https://www.radiookapi.net/actualite | +| mediacongo.cd | +100k | https://www.mediacongo.net/ | +| beto.cd | +30k | https://www.beto.cd/ | +| actualite.cd | +57k | https://actualite.cd/ | +| 7sur7.cd | +50k | https://7sur7.cd | +| newscd.net | +5k | https://newscd.net | +| congoindependant.com | +10k | https://www.congoindependant.com/ | +| congoactu.net | +10k | https://www.congoactu.net/ | -Each package/app is 100% [TypeScript](https://www.typescriptlang.org/). - -### Utilities - -This Turborepo has some additional tools already setup for you: - -- [TypeScript](https://www.typescriptlang.org/) for static type checking -- [ESLint](https://eslint.org/) for code linting -- [Prettier](https://prettier.io) for code formatting - -### Build - -To build all apps and packages, run the following command: - -``` -cd my-turborepo - -# With [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation) installed (recommended) -turbo build - -# Without [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation), use your package manager -npx turbo build -yarn dlx turbo build -pnpm exec turbo build -``` - -You can build a specific package by using a [filter](https://turborepo.com/docs/crafting-your-repository/running-tasks#using-filters): - -``` -# With [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation) installed (recommended) -turbo build --filter=docs - -# Without [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation), use your package manager -npx turbo build --filter=docs -yarn exec turbo build --filter=docs -pnpm exec turbo build --filter=docs -``` - -### Develop - -To develop all apps and packages, run the following command: - -``` -cd my-turborepo - -# With [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation) installed (recommended) -turbo dev - -# Without [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation), use your package manager -npx turbo dev -yarn exec turbo dev -pnpm exec turbo dev -``` - -You can develop a specific package by using a [filter](https://turborepo.com/docs/crafting-your-repository/running-tasks#using-filters): - -``` -# With [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation) installed (recommended) -turbo dev --filter=web - -# Without [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation), use your package manager -npx turbo dev --filter=web -yarn exec turbo dev --filter=web -pnpm exec turbo dev --filter=web -``` - -### Remote Caching - -> [!TIP] -> Vercel Remote Cache is free for all plans. Get started today at [vercel.com](https://vercel.com/signup?/signup?utm_source=remote-cache-sdk&utm_campaign=free_remote_cache). - -Turborepo can use a technique known as [Remote Caching](https://turborepo.com/docs/core-concepts/remote-caching) to share cache artifacts across machines, enabling you to share build caches with your team and CI/CD pipelines. - -By default, Turborepo will cache locally. To enable Remote Caching you will need an account with Vercel. If you don't have an account you can [create one](https://vercel.com/signup?utm_source=turborepo-examples), then enter the following commands: - -``` -cd my-turborepo - -# With [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation) installed (recommended) -turbo login - -# Without [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation), use your package manager -npx turbo login -yarn exec turbo login -pnpm exec turbo login -``` - -This will authenticate the Turborepo CLI with your [Vercel account](https://vercel.com/docs/concepts/personal-accounts/overview). - -Next, you can link your Turborepo to your Remote Cache by running the following command from the root of your Turborepo: - -``` -# With [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation) installed (recommended) -turbo link - -# Without [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation), use your package manager -npx turbo link -yarn exec turbo link -pnpm exec turbo link -``` - -## Useful Links - -Learn more about the power of Turborepo: - -- [Tasks](https://turborepo.com/docs/crafting-your-repository/running-tasks) -- [Caching](https://turborepo.com/docs/crafting-your-repository/caching) -- [Remote Caching](https://turborepo.com/docs/core-concepts/remote-caching) -- [Filtering](https://turborepo.com/docs/crafting-your-repository/running-tasks#using-filters) -- [Configuration Options](https://turborepo.com/docs/reference/configuration) -- [CLI Usage](https://turborepo.com/docs/reference/command-line-reference) +### Acknowledgment: +The compilation and curation of the "Basango" were conducted by Bernard Ngandu with the primary objective of facilitating research and analysis related to the Democratic Republic of Congo. +I do not own the content of the articles, and all rights belong to the respective publishers. The dataset is intended for non-commercial research purposes only. diff --git a/basango/SECURITY.md b/basango/SECURITY.md new file mode 100644 index 0000000..23a935f --- /dev/null +++ b/basango/SECURITY.md @@ -0,0 +1,57 @@ +# Security + +Contact: [security@basango.com](mailto:security@basango.com) + +Based on [https://supabase.com/.well-known/security.txt](https://supabase.com/.well-known/security.txt) + +At Midday, we consider the security of our systems a top priority. But no matter +how much effort we put into system security, there can still be vulnerabilities +present. + +If you discover a vulnerability, we would like to know about it so we can take +steps to address it as quickly as possible. We would like to ask you to help us +better protect our clients and our systems. + +## Out of scope vulnerabilities + +- Clickjacking on pages with no sensitive actions. +- Unauthenticated/logout/login CSRF. +- Attacks requiring MITM or physical access to a user's device. +- Any activity that could lead to the disruption of our service (DoS). +- Content spoofing and text injection issues without showing an attack + vector/without being able to modify HTML/CSS. +- Email spoofing +- Missing DNSSEC, CAA, CSP headers +- Lack of Secure or HTTP only flag on non-sensitive cookies +- Deadlinks + +## Please do the following + +- E-mail your findings to [security@basango.com](mailto:security@mbasango.com). +- Do not run automated scanners on our infrastructure or dashboard. If you wish + to do this, contact us and we will set up a sandbox for you. +- Do not take advantage of the vulnerability or problem you have discovered, + for example by downloading more data than necessary to demonstrate the + vulnerability or deleting or modifying other people's data, +- Do not reveal the problem to others until it has been resolved, +- Do not use attacks on physical security, social engineering, distributed + denial of service, spam or applications of third parties, +- Do provide sufficient information to reproduce the problem, so we will be + able to resolve it as quickly as possible. Usually, the IP address or the URL + of the affected system and a description of the vulnerability will be + sufficient, but complex vulnerabilities may require further explanation. + +## What we promise + +- We will respond to your report within 3 business days with our evaluation of + the report and an expected resolution date, +- If you have followed the instructions above, we will not take any legal + action against you in regard to the report, +- We will handle your report with strict confidentiality, and not pass on your + personal details to third parties without your permission, +- We will keep you informed of the progress towards resolving the problem, +- In the public information concerning the problem reported, we will give your + name as the discoverer of the problem (unless you desire otherwise), and +- We strive to resolve all problems as quickly as possible, and we would like + to play an active role in the ultimate publication on the problem after it + is resolved. \ No newline at end of file diff --git a/basango/apps/crawler/package.json b/basango/apps/crawler/package.json new file mode 100644 index 0000000..87d3d0e --- /dev/null +++ b/basango/apps/crawler/package.json @@ -0,0 +1,19 @@ +{ + "name": "@basango/crawler", + "version": "0.1.0", + "private": true, + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc -b", + "test": "vitest --run" + }, + "dependencies": { + "bullmq": "^4.17.0", + "date-fns": "^3.6.0", + "ioredis": "^5.3.2", + "tiktoken": "^1.0.14", + "zod": "^4.0.0" + } +} diff --git a/basango/apps/crawler/src/__tests__/config.test.ts b/basango/apps/crawler/src/__tests__/config.test.ts new file mode 100644 index 0000000..8aa014c --- /dev/null +++ b/basango/apps/crawler/src/__tests__/config.test.ts @@ -0,0 +1,81 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +import { describe, expect, it } from "vitest"; + +import { loadConfig } from "./config"; +import { resolveConfigPath } from "./schema"; + +describe("loadConfig", () => { + it("parses json configuration and ensures directories", () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "crawler-config-")); + const paths = { + root: tempDir, + data: path.join(tempDir, "data"), + logs: path.join(tempDir, "logs"), + configs: path.join(tempDir, "configs"), + }; + + const configPath = path.join(tempDir, "pipeline.json"); + fs.writeFileSync( + configPath, + JSON.stringify( + { + paths, + fetch: { + client: { timeout: 10 }, + }, + }, + null, + 2, + ), + ); + + const config = loadConfig({ configPath }); + + expect(config.fetch.client.timeout).toBe(10); + expect(fs.existsSync(paths.data)).toBe(true); + expect(fs.existsSync(paths.logs)).toBe(true); + expect(fs.existsSync(paths.configs)).toBe(true); + }); + + it("merges environment override if available", () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "crawler-config-")); + const paths = { + root: tempDir, + data: path.join(tempDir, "data"), + logs: path.join(tempDir, "logs"), + configs: path.join(tempDir, "configs"), + }; + + const basePath = path.join(tempDir, "pipeline.json"); + fs.writeFileSync( + basePath, + JSON.stringify( + { + paths, + logging: { level: "INFO" }, + }, + null, + 2, + ), + ); + + const overridePath = resolveConfigPath(basePath, "production"); + fs.writeFileSync( + overridePath, + JSON.stringify( + { + logging: { level: "DEBUG" }, + }, + null, + 2, + ), + ); + + const config = loadConfig({ configPath: basePath, env: "production" }); + + expect(config.logging.level).toBe("DEBUG"); + }); +}); diff --git a/basango/apps/crawler/src/__tests__/queue.test.ts b/basango/apps/crawler/src/__tests__/queue.test.ts new file mode 100644 index 0000000..fc2c5cc --- /dev/null +++ b/basango/apps/crawler/src/__tests__/queue.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from "vitest"; + +import { createQueueManager, createQueueSettings } from "./queue"; + +class InMemoryQueue { + public jobs: Array<{ name: string; data: unknown }> = []; + + async add(name: string, data: unknown) { + this.jobs.push({ name, data }); + return { id: `${name}-${this.jobs.length}` }; + } +} + +describe("createQueueManager", () => { + it("prefixes queue names", () => { + const manager = createQueueManager({ + settings: createQueueSettings({ prefix: "test" }), + queueFactory: (queueName) => { + expect(queueName).toBe("listing"); + return new InMemoryQueue(); + }, + connection: { + quit: async () => undefined, + } as any, + }); + + expect(manager.iterQueueNames()).toEqual([ + "test:listing", + "test:articles", + "test:processed", + ]); + }); + + it("enqueues listing job with validated payload", async () => { + const queue = new InMemoryQueue(); + const manager = createQueueManager({ + queueFactory: () => queue, + connection: { quit: async () => undefined } as any, + }); + + const job = await manager.enqueueListing({ + source_id: "radiookapi", + env: "test", + }); + + expect(job.id).toBe("collect_listing-1"); + expect(queue.jobs[0]).toEqual({ + name: "collect_listing", + data: { + source_id: "radiookapi", + env: "test", + page_range: undefined, + date_range: undefined, + category: undefined, + }, + }); + }); +}); diff --git a/basango/apps/crawler/src/__tests__/schema.test.ts b/basango/apps/crawler/src/__tests__/schema.test.ts new file mode 100644 index 0000000..db02185 --- /dev/null +++ b/basango/apps/crawler/src/__tests__/schema.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from "vitest"; + +import { + PipelineConfigSchema, + createDateRange, + formatDateRange, + isTimestampInRange, + PageRangeSpecSchema, + PageRangeSchema, + schemaToJSON, +} from "./schema"; + +describe("schema helpers", () => { + it("creates date range from spec", () => { + const range = createDateRange("2024-01-01:2024-01-31"); + expect(range.start).toBeLessThan(range.end); + expect(formatDateRange(range)).toBe("2024-01-01:2024-01-31"); + }); + + it("checks membership", () => { + const range = createDateRange("2024-01-01:2024-01-02"); + expect(isTimestampInRange(range, range.start)).toBe(true); + expect(isTimestampInRange(range, range.start - 1)).toBe(false); + }); + + it("parses page range spec", () => { + const range = PageRangeSchema.parse(PageRangeSpecSchema.parse("1:10")); + expect(range).toEqual({ start: 1, end: 10 }); + }); + + it("produces json schema", () => { + const json = schemaToJSON(PipelineConfigSchema); + expect(json.type).toBe("object"); + }); +}); diff --git a/basango/apps/crawler/src/__tests__/tasks.test.ts b/basango/apps/crawler/src/__tests__/tasks.test.ts new file mode 100644 index 0000000..c779fa2 --- /dev/null +++ b/basango/apps/crawler/src/__tests__/tasks.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it, vi } from "vitest"; + +import { + scheduleAsyncCrawl, + registerCrawlerTaskHandlers, + collectListing, +} from "./tasks"; +import { QueueManager } from "./queue"; + +describe("Async tasks", () => { + it("schedules crawl with provided manager", async () => { + const enqueueListing = vi.fn().mockResolvedValue({ id: "job-1" }); + const manager = { + enqueueListing, + } as unknown as QueueManager; + + const jobId = await scheduleAsyncCrawl({ + sourceId: "radiookapi", + queueManager: manager, + }); + + expect(jobId).toBe("job-1"); + expect(enqueueListing).toHaveBeenCalledWith({ + source_id: "radiookapi", + env: "development", + page_range: undefined, + date_range: undefined, + category: undefined, + }); + }); + + it("delegates listing collection to registered handler", async () => { + const handler = vi.fn().mockResolvedValue(5); + registerCrawlerTaskHandlers({ collectListing: handler }); + + const count = await collectListing({ + source_id: "radiookapi", + env: "development", + }); + + expect(count).toBe(5); + expect(handler).toHaveBeenCalledWith({ + source_id: "radiookapi", + env: "development", + page_range: undefined, + date_range: undefined, + category: undefined, + }); + }); +}); diff --git a/basango/apps/crawler/src/config.ts b/basango/apps/crawler/src/config.ts new file mode 100644 index 0000000..a0f4147 --- /dev/null +++ b/basango/apps/crawler/src/config.ts @@ -0,0 +1,91 @@ +import fs from "node:fs"; +import path from "node:path"; + +import { + PipelineConfig, + PipelineConfigSchema, + mergePipelineConfig, + resolveConfigPath, + resolveProjectPaths, +} from "./schema"; +import { ensureDirectories } from "./utils"; + +export interface LoadConfigOptions { + configPath?: string; + env?: string; +} + +const DEFAULT_CONFIG_FILES = [ + path.join(process.cwd(), "config", "pipeline.json"), + path.join(process.cwd(), "pipeline.json"), +]; + +const readJsonFile = (filePath: string): unknown => { + const contents = fs.readFileSync(filePath, "utf-8"); + return contents.trim() === "" ? {} : JSON.parse(contents); +}; + +const locateConfigFile = (explicit?: string): string => { + if (explicit && fs.existsSync(explicit)) { + return explicit; + } + + for (const candidate of DEFAULT_CONFIG_FILES) { + if (fs.existsSync(candidate)) { + return candidate; + } + } + + return DEFAULT_CONFIG_FILES[0]; +}; + +const readPipelineConfig = (configPath: string): PipelineConfig => { + if (!fs.existsSync(configPath)) { + return PipelineConfigSchema.parse({ + paths: resolveProjectPaths(path.resolve(".")), + }); + } + + const raw = readJsonFile(configPath); + return PipelineConfigSchema.parse(raw); +}; + +const applyEnvironmentOverride = ( + baseConfig: PipelineConfig, + basePath: string, + env?: string, +): PipelineConfig => { + if (!env || env === "development") { + return baseConfig; + } + + const overridePath = resolveConfigPath(basePath, env); + if (!fs.existsSync(overridePath)) { + return baseConfig; + } + + const overrides = PipelineConfigSchema.parse(readJsonFile(overridePath)); + return mergePipelineConfig(baseConfig, overrides); +}; + +export const loadConfig = (options: LoadConfigOptions = {}): PipelineConfig => { + const basePath = locateConfigFile(options.configPath); + const config = applyEnvironmentOverride( + readPipelineConfig(basePath), + basePath, + options.env, + ); + + ensureDirectories(config.paths); + return config; +}; + +export const dumpConfig = ( + config: PipelineConfig, + targetPath?: string, +): void => { + const destination = targetPath ?? locateConfigFile(); + const normalized = PipelineConfigSchema.parse(config); + fs.mkdirSync(path.dirname(destination), { recursive: true }); + fs.writeFileSync(destination, JSON.stringify(normalized, null, 2)); +}; diff --git a/apps/crawler/src/index.ts b/basango/apps/crawler/src/index.ts similarity index 100% rename from apps/crawler/src/index.ts rename to basango/apps/crawler/src/index.ts diff --git a/basango/apps/crawler/src/schema.ts b/basango/apps/crawler/src/schema.ts new file mode 100644 index 0000000..060e493 --- /dev/null +++ b/basango/apps/crawler/src/schema.ts @@ -0,0 +1,314 @@ +import path from "node:path"; + +import { getUnixTime, parse, isMatch, format as formatDate } from "date-fns"; +import { z } from "zod"; + +export const UpdateDirectionSchema = z.enum(["forward", "backward"]); +export type UpdateDirection = z.infer; + +export const SourceKindSchema = z.enum(["wordpress", "html"]); +export type SourceKind = z.infer; + +export const SourceDateSchema = z.object({ + format: z.string().default("yyyy-LL-dd HH:mm"), + pattern: z.string().nullable().optional(), + replacement: z.string().nullable().optional(), +}); +export type SourceDate = z.infer; + +export const SourceSelectorsSchema = z.object({ + articles: z.string().optional().nullable(), + article_title: z.string().optional().nullable(), + article_link: z.string().optional().nullable(), + article_body: z.string().optional().nullable(), + article_date: z.string().optional().nullable(), + article_categories: z.string().optional().nullable(), + pagination: z.string().default("ul.pagination > li a"), +}); +export type SourceSelectors = z.infer; + +const BaseSourceSchema = z.object({ + source_id: z.string(), + source_url: z.string().url(), + source_date: SourceDateSchema.default(SourceDateSchema.parse({})), + source_kind: SourceKindSchema, + categories: z.array(z.string()).default([]), + supports_categories: z.boolean().default(false), + requires_details: z.boolean().default(false), + requires_rate_limit: z.boolean().default(false), +}); + +export const HtmlSourceConfigSchema = BaseSourceSchema.extend({ + source_kind: z.literal("html"), + source_selectors: SourceSelectorsSchema.default( + SourceSelectorsSchema.parse({}), + ), + pagination_template: z.string(), +}); + +export const WordPressSourceConfigSchema = BaseSourceSchema.extend({ + source_kind: z.literal("wordpress"), + source_date: SourceDateSchema.default( + SourceDateSchema.parse({ format: "yyyy-LL-dd'T'HH:mm:ss" }), + ), +}); + +export type HtmlSourceConfig = z.infer; +export type WordPressSourceConfig = z.infer; +export type AnySourceConfig = HtmlSourceConfig | WordPressSourceConfig; + +export const DateRangeSchema = z + .object({ + start: z.number().int(), + end: z.number().int(), + }) + .superRefine((value, ctx) => { + if (value.start === 0 || value.end === 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Timestamp cannot be zero", + }); + } + if (value.end < value.start) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "End timestamp must be greater than or equal to start", + }); + } + }); + +export type DateRange = z.infer; + +export const PageRangeSchema = z + .object({ + start: z.number().int().min(0), + end: z.number().int().min(0), + }) + .superRefine((value, ctx) => { + if (value.end < value.start) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "End page must be greater than or equal to start page", + }); + } + }); + +export type PageRange = z.infer; + +export const PageRangeSpecSchema = z + .string() + .regex(/^[0-9]+:[0-9]+$/, "Invalid page range format. Use start:end") + .transform((spec) => { + const [startText, endText] = spec.split(":"); + return { + start: Number.parseInt(startText, 10), + end: Number.parseInt(endText, 10), + }; + }); + +const defaultDateFormat = "yyyy-LL-dd"; + +export const DateRangeSpecSchema = z + .string() + .regex(/.+:.+/, "Expected start:end format") + .transform((spec) => { + const [startRaw, endRaw] = spec.split(":"); + return { startRaw, endRaw }; + }); + +const parseDate = (value: string, format: string): Date => { + if (!isMatch(value, format)) { + throw new Error(`Invalid date '${value}' for format '${format}'`); + } + const parsed = parse(value, format, new Date()); + if (Number.isNaN(parsed.getTime())) { + throw new Error(`Invalid date '${value}' for format '${format}'`); + } + return parsed; +}; + +export interface CreateDateRangeOptions { + format?: string; + separator?: string; +} + +export const createDateRange = ( + spec: string, + options: CreateDateRangeOptions = {}, +): DateRange => { + const { format = defaultDateFormat, separator = ":" } = options; + if (!separator) { + throw new Error("Separator cannot be empty"); + } + + const normalized = spec.replace(separator, ":"); + const parsedSpec = DateRangeSpecSchema.parse(normalized); + + const startDate = parseDate(parsedSpec.startRaw, format); + const endDate = parseDate(parsedSpec.endRaw, format); + + const range = { + start: getUnixTime(startDate), + end: getUnixTime(endDate), + }; + + return DateRangeSchema.parse(range); +}; + +export const formatDateRange = ( + range: DateRange, + fmt = defaultDateFormat, +): string => { + const start = formatDate(new Date(range.start * 1000), fmt); + const end = formatDate(new Date(range.end * 1000), fmt); + return `${start}:${end}`; +}; + +export const isTimestampInRange = ( + range: DateRange, + timestamp: number, +): boolean => { + return range.start <= timestamp && timestamp <= range.end; +}; + +export const ProjectPathsSchema = z.object({ + root: z.string(), + data: z.string(), + logs: z.string(), + configs: z.string(), +}); +export type ProjectPaths = z.infer; + +export const resolveProjectPaths = (rootDir: string): ProjectPaths => { + return ProjectPathsSchema.parse({ + root: rootDir, + data: path.join(rootDir, "data", "dataset"), + logs: path.join(rootDir, "data", "logs"), + configs: path.join(rootDir, "config"), + }); +}; + +export const LoggingConfigSchema = z.object({ + level: z.string().default("INFO"), + format: z + .string() + .default("%(asctime)s - %(name)s - %(levelname)s - %(message)s"), + console_logging: z.boolean().default(true), + file_logging: z.boolean().default(false), + log_file: z.string().default("crawler.log"), + max_log_size: z + .number() + .int() + .positive() + .default(10 * 1024 * 1024), + backup_count: z.number().int().nonnegative().default(5), +}); +export type LoggingConfig = z.infer; + +export const ClientConfigSchema = z.object({ + timeout: z.number().positive().default(20), + user_agent: z + .string() + .default("Basango/0.1 (+https://github.com/bernard-ng/basango)"), + follow_redirects: z.boolean().default(true), + verify_ssl: z.boolean().default(true), + rotate: z.boolean().default(true), + max_retries: z.number().int().nonnegative().default(3), + backoff_initial: z.number().nonnegative().default(1), + backoff_multiplier: z.number().positive().default(2), + backoff_max: z.number().nonnegative().default(30), + respect_retry_after: z.boolean().default(true), +}); + +export const CrawlerConfigSchema = z.object({ + source: z + .union([HtmlSourceConfigSchema, WordPressSourceConfigSchema]) + .optional(), + page_range: PageRangeSchema.optional(), + date_range: DateRangeSchema.optional(), + category: z.string().optional(), + notify: z.boolean().default(false), + is_update: z.boolean().default(false), + use_multi_threading: z.boolean().default(false), + max_workers: z.number().int().positive().default(5), + direction: UpdateDirectionSchema.default("forward"), +}); + +export type ClientConfig = z.infer; +export type CrawlerConfig = z.infer & { + source?: AnySourceConfig; +}; + +export const FetchConfigSchema = z.object({ + client: ClientConfigSchema.default(ClientConfigSchema.parse({})), + crawler: CrawlerConfigSchema.default(CrawlerConfigSchema.parse({})), +}); +export type FetchConfig = z.infer; + +const SourcesConfigSchema = z.object({ + html: z.array(HtmlSourceConfigSchema).default([]), + wordpress: z.array(WordPressSourceConfigSchema).default([]), +}); + +export type SourcesConfig = z.infer & { + find: (sourceId: string) => AnySourceConfig | undefined; +}; + +export const createSourcesConfig = (input: unknown): SourcesConfig => { + const parsed = SourcesConfigSchema.parse(input); + const resolver = (sourceId: string) => + [...parsed.html, ...parsed.wordpress].find( + (source) => source.source_id === sourceId, + ); + return Object.assign({ find: resolver }, parsed); +}; + +export const PipelineConfigSchema = z.object({ + paths: ProjectPathsSchema.default(resolveProjectPaths(process.cwd())), + logging: LoggingConfigSchema.default(LoggingConfigSchema.parse({})), + fetch: FetchConfigSchema.default(FetchConfigSchema.parse({})), + sources: z + .union([SourcesConfigSchema, z.undefined()]) + .transform((value) => createSourcesConfig(value ?? {})), +}); + +export type PipelineConfig = z.infer & { + sources: SourcesConfig; +}; + +export const mergePipelineConfig = ( + base: PipelineConfig, + overrides: Partial, +): PipelineConfig => { + const paths = overrides.paths ?? base.paths; + const logging = { ...base.logging, ...(overrides.logging ?? {}) }; + const fetch = { + client: { ...base.fetch.client, ...(overrides.fetch?.client ?? {}) }, + crawler: { ...base.fetch.crawler, ...(overrides.fetch?.crawler ?? {}) }, + }; + + const sources = createSourcesConfig({ + html: overrides.sources?.html ?? base.sources.html, + wordpress: overrides.sources?.wordpress ?? base.sources.wordpress, + }); + + return { + paths, + logging, + fetch, + sources, + }; +}; + +export const resolveConfigPath = (basePath: string, env?: string): string => { + if (!env || env === "development") { + return basePath; + } + + const ext = path.extname(basePath); + const withoutExt = basePath.slice(0, basePath.length - ext.length); + return `${withoutExt}.${env}${ext}`; +}; + +export const schemaToJSON = (schema: T) => + schema.toJSON(); diff --git a/basango/apps/crawler/src/services/crawler/async/queue.ts b/basango/apps/crawler/src/services/crawler/async/queue.ts new file mode 100644 index 0000000..262e414 --- /dev/null +++ b/basango/apps/crawler/src/services/crawler/async/queue.ts @@ -0,0 +1,142 @@ +import { randomUUID } from "node:crypto"; + +import IORedis from "ioredis"; +import { Queue, JobsOptions, QueueOptions } from "bullmq"; +import { z } from "zod"; + +import { + ListingTaskPayload, + ArticleTaskPayload, + ProcessedTaskPayload, + ListingTaskPayloadSchema, + ArticleTaskPayloadSchema, + ProcessedTaskPayloadSchema, +} from "./schemas"; +import { parseRedisUrl } from "../../../utils"; + +const QueueSettingsSchema = z.object({ + redis_url: z + .string() + .default(process.env.BASANGO_REDIS_URL ?? "redis://localhost:6379/0"), + prefix: z.string().default(process.env.BASANGO_QUEUE_PREFIX ?? "crawler"), + default_timeout: z + .number() + .int() + .positive() + .default(Number(process.env.BASANGO_QUEUE_TIMEOUT ?? 600)), + result_ttl: z + .number() + .int() + .nonnegative() + .default(Number(process.env.BASANGO_QUEUE_RESULT_TTL ?? 3600)), + failure_ttl: z + .number() + .int() + .nonnegative() + .default(Number(process.env.BASANGO_QUEUE_FAILURE_TTL ?? 3600)), + listing_queue: z.string().default("listing"), + article_queue: z.string().default("articles"), + processed_queue: z.string().default("processed"), +}); + +export type QueueSettingsInput = z.input; +export type QueueSettings = z.output; + +export const createQueueSettings = ( + input?: QueueSettingsInput, +): QueueSettings => QueueSettingsSchema.parse(input ?? {}); + +export interface QueueBackend { + add: (name: string, data: T, opts?: JobsOptions) => Promise<{ id: string }>; +} + +export type QueueFactory = ( + queueName: string, + settings: QueueSettings, + connection?: IORedis, +) => QueueBackend; + +const defaultQueueFactory: QueueFactory = (queueName, settings, connection) => { + const redisConnection = + connection ?? + new IORedis(settings.redis_url, parseRedisUrl(settings.redis_url)); + const options: QueueOptions = { + connection: redisConnection, + prefix: settings.prefix, + }; + + const queue = new Queue(queueName, options); + return { + add: async (name, data, opts) => { + const job = await queue.add(name, data, { + removeOnComplete: settings.result_ttl === 0 ? true : undefined, + removeOnFail: settings.failure_ttl === 0 ? true : undefined, + timeout: settings.default_timeout * 1000, + ...opts, + }); + return { id: job.id ?? randomUUID() }; + }, + }; +}; + +export interface CreateQueueManagerOptions { + settings?: QueueSettings | QueueSettingsInput; + queueFactory?: QueueFactory; + connection?: IORedis; +} + +export interface QueueManager { + readonly settings: QueueSettings; + readonly connection: IORedis; + enqueueListing: (payload: ListingTaskPayload) => Promise<{ id: string }>; + enqueueArticle: (payload: ArticleTaskPayload) => Promise<{ id: string }>; + enqueueProcessed: (payload: ProcessedTaskPayload) => Promise<{ id: string }>; + iterQueueNames: () => string[]; + queueName: (suffix: string) => string; + close: () => Promise; +} + +export const createQueueManager = ( + options: CreateQueueManagerOptions = {}, +): QueueManager => { + const settings = createQueueSettings( + options.settings as QueueSettingsInput | undefined, + ); + + const connection = + options.connection ?? + new IORedis(settings.redis_url, parseRedisUrl(settings.redis_url)); + const factory = options.queueFactory ?? defaultQueueFactory; + + const ensureQueue = (queueName: string) => + factory(queueName, settings, connection); + + return { + settings, + connection, + enqueueListing: (payload) => { + const data = ListingTaskPayloadSchema.parse(payload); + const queue = ensureQueue(settings.listing_queue); + return queue.add("collect_listing", data); + }, + enqueueArticle: (payload) => { + const data = ArticleTaskPayloadSchema.parse(payload); + const queue = ensureQueue(settings.article_queue); + return queue.add("collect_article", data); + }, + enqueueProcessed: (payload) => { + const data = ProcessedTaskPayloadSchema.parse(payload); + const queue = ensureQueue(settings.processed_queue); + return queue.add("forward_for_processing", data); + }, + iterQueueNames: () => [ + `${settings.prefix}:${settings.listing_queue}`, + `${settings.prefix}:${settings.article_queue}`, + `${settings.prefix}:${settings.processed_queue}`, + ], + queueName: (suffix: string) => `${settings.prefix}:${suffix}`, + close: async () => { + await connection.quit(); + }, + }; +}; diff --git a/basango/apps/crawler/src/services/crawler/async/schemas.ts b/basango/apps/crawler/src/services/crawler/async/schemas.ts new file mode 100644 index 0000000..dac5a0a --- /dev/null +++ b/basango/apps/crawler/src/services/crawler/async/schemas.ts @@ -0,0 +1,41 @@ +import { z } from "zod"; + +import { + AnySourceConfig, + DateRangeSchema, + PageRangeSchema, +} from "../../../schema"; + +export const ListingTaskPayloadSchema = z.object({ + source_id: z.string(), + env: z.string().default("development"), + page_range: z.string().optional().nullable(), + date_range: z.string().optional().nullable(), + category: z.string().optional().nullable(), +}); + +export type ListingTaskPayload = z.infer; + +export const ArticleTaskPayloadSchema = z.object({ + source_id: z.string(), + env: z.string().default("development"), + url: z.string().url(), + page: z.number().int().nonnegative().optional(), + page_range: PageRangeSchema.optional().nullable(), + date_range: DateRangeSchema.optional().nullable(), + category: z.string().optional().nullable(), +}); + +export type ArticleTaskPayload = z.infer; + +export const ProcessedTaskPayloadSchema = z.object({ + source_id: z.string(), + env: z.string().default("development"), + article: z.any(), +}); + +export type ProcessedTaskPayload = z.infer; + +export interface ListingContext { + source: AnySourceConfig; +} diff --git a/basango/apps/crawler/src/services/crawler/async/tasks.ts b/basango/apps/crawler/src/services/crawler/async/tasks.ts new file mode 100644 index 0000000..df86e5d --- /dev/null +++ b/basango/apps/crawler/src/services/crawler/async/tasks.ts @@ -0,0 +1,94 @@ +import { + ListingTaskPayloadSchema, + ArticleTaskPayloadSchema, + ProcessedTaskPayloadSchema, + ListingTaskPayload, + ArticleTaskPayload, + ProcessedTaskPayload, +} from "./schemas"; +import { + createQueueManager, + QueueManager, + QueueSettings, + QueueSettingsInput, +} from "./queue"; + +export interface CrawlerTaskHandlers { + collectListing: (payload: ListingTaskPayload) => Promise | number; + collectArticle: (payload: ArticleTaskPayload) => Promise | unknown; + forwardForProcessing: ( + payload: ProcessedTaskPayload, + ) => Promise | unknown; +} + +const notImplemented = (name: keyof CrawlerTaskHandlers) => () => { + throw new Error(`Crawler task handler '${name}' is not implemented`); +}; + +let handlers: CrawlerTaskHandlers = { + collectListing: notImplemented("collectListing"), + collectArticle: notImplemented("collectArticle"), + forwardForProcessing: notImplemented("forwardForProcessing"), +}; + +export const registerCrawlerTaskHandlers = ( + overrides: Partial, +): void => { + handlers = { ...handlers, ...overrides }; +}; + +export interface ScheduleAsyncCrawlOptions { + sourceId: string; + env?: string; + pageRange?: string | null; + dateRange?: string | null; + category?: string | null; + settings?: QueueSettings | QueueSettingsInput; + queueManager?: QueueManager; +} + +export const scheduleAsyncCrawl = async ({ + sourceId, + env = "development", + pageRange, + dateRange, + category, + settings, + queueManager, +}: ScheduleAsyncCrawlOptions): Promise => { + const payload = ListingTaskPayloadSchema.parse({ + source_id: sourceId, + env, + page_range: pageRange ?? undefined, + date_range: dateRange ?? undefined, + category: category ?? undefined, + }); + + const manager = queueManager ?? createQueueManager({ settings }); + try { + const job = await manager.enqueueListing(payload); + return job.id; + } finally { + if (!queueManager) { + await manager.close(); + } + } +}; + +export const collectListing = async (payload: unknown): Promise => { + const data = ListingTaskPayloadSchema.parse(payload); + const result = await handlers.collectListing(data); + return typeof result === "number" ? result : 0; +}; + +export const collectArticle = async (payload: unknown): Promise => { + const data = ArticleTaskPayloadSchema.parse(payload); + return handlers.collectArticle(data); +}; + +export const forwardForProcessing = async ( + payload: unknown, +): Promise => { + const data = ProcessedTaskPayloadSchema.parse(payload); + return handlers.forwardForProcessing(data); +}; diff --git a/basango/apps/crawler/src/services/crawler/async/worker.ts b/basango/apps/crawler/src/services/crawler/async/worker.ts new file mode 100644 index 0000000..4fe3ee0 --- /dev/null +++ b/basango/apps/crawler/src/services/crawler/async/worker.ts @@ -0,0 +1,87 @@ +import IORedis from "ioredis"; +import { Worker, QueueEvents } from "bullmq"; + +import { + createQueueManager, + QueueFactory, + QueueManager, + QueueSettings, + QueueSettingsInput, +} from "./queue"; +import { collectArticle, collectListing, forwardForProcessing } from "./tasks"; + +export interface WorkerOptions { + queueNames?: string[]; + settings?: QueueSettings | QueueSettingsInput; + connection?: IORedis; + queueFactory?: QueueFactory; + concurrency?: number; + onError?: (error: Error) => void; + queueManager?: QueueManager; +} + +export interface WorkerHandle { + readonly workers: Worker[]; + readonly events: QueueEvents[]; + close: () => Promise; +} + +export const startWorker = (options: WorkerOptions = {}): WorkerHandle => { + const manager = + options.queueManager ?? + createQueueManager({ + settings: options.settings, + connection: options.connection, + queueFactory: options.queueFactory, + }); + + const queueNames = options.queueNames ?? manager.iterQueueNames(); + const workers: Worker[] = []; + const events: QueueEvents[] = []; + + const connection = manager.connection; + + for (const queueName of queueNames) { + const worker = new Worker( + queueName, + async (job) => { + switch (job.name) { + case "collect_listing": + return collectListing(job.data); + case "collect_article": + return collectArticle(job.data); + case "forward_for_processing": + return forwardForProcessing(job.data); + default: + throw new Error(`Unknown job name: ${job.name}`); + } + }, + { + connection, + concurrency: options.concurrency ?? 5, + }, + ); + + if (options.onError) { + worker.on("failed", (_, err) => options.onError?.(err as Error)); + worker.on("error", (err) => options.onError?.(err as Error)); + } + + const queueEvents = new QueueEvents(queueName, { connection }); + + workers.push(worker); + events.push(queueEvents); + } + + return { + workers, + events, + close: async () => { + await Promise.all(workers.map((worker) => worker.close())); + await Promise.all(events.map((event) => event.close())); + if (!options.queueManager) { + await manager.close(); + } + }, + }; +}; diff --git a/apps/crawler/src/services/crawler/index.ts b/basango/apps/crawler/src/services/crawler/index.ts similarity index 100% rename from apps/crawler/src/services/crawler/index.ts rename to basango/apps/crawler/src/services/crawler/index.ts diff --git a/basango/apps/crawler/src/utils.ts b/basango/apps/crawler/src/utils.ts new file mode 100644 index 0000000..f55c04d --- /dev/null +++ b/basango/apps/crawler/src/utils.ts @@ -0,0 +1,38 @@ +import fs from "node:fs"; + +import type { RedisOptions } from "ioredis"; +import { get_encoding } from "tiktoken"; + +import type { ProjectPaths } from "@/schema"; + +export const ensureDirectories = (paths: ProjectPaths): void => { + for (const dir of [paths.data, paths.logs, paths.configs]) { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + } +}; + +export const parseRedisUrl = (url: string): RedisOptions => { + if (!url.startsWith("redis://")) { + return {}; + } + const parsed = new URL(url); + return { + host: parsed.hostname, + port: Number(parsed.port || 6379), + password: parsed.password || undefined, + db: Number(parsed.pathname?.replace("/", "") || 0), + }; +}; + +export const countTokens = (text: string, encoding = "cl100k_base"): number => { + try { + const encoder = get_encoding(encoding); + const tokens = encoder.encode(text); + encoder.free(); + return tokens.length; + } catch { + return text.length; + } +}; diff --git a/basango/apps/crawler/tsconfig.json b/basango/apps/crawler/tsconfig.json new file mode 100644 index 0000000..f11a34a --- /dev/null +++ b/basango/apps/crawler/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@basango/tsconfig/base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src"], + "references": [], + "paths": { + "@basango/crawler": ["src/**"] + } +} diff --git a/basango/apps/crawler/vitest.config.ts b/basango/apps/crawler/vitest.config.ts new file mode 100644 index 0000000..acf96a1 --- /dev/null +++ b/basango/apps/crawler/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + globals: true, + include: ["src/**/*.test.ts"], + }, +}); diff --git a/basango/apps/web/.gitignore b/basango/apps/web/.gitignore deleted file mode 100644 index f886745..0000000 --- a/basango/apps/web/.gitignore +++ /dev/null @@ -1,36 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js -.yarn/install-state.gz - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# env files (can opt-in for commiting if needed) -.env* - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts diff --git a/basango/apps/web/README.md b/basango/apps/web/README.md deleted file mode 100644 index a98bfa8..0000000 --- a/basango/apps/web/README.md +++ /dev/null @@ -1,36 +0,0 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load Inter, a custom Google Font. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/basango/apps/web/app/favicon.ico b/basango/apps/web/app/favicon.ico deleted file mode 100644 index 718d6fe..0000000 Binary files a/basango/apps/web/app/favicon.ico and /dev/null differ diff --git a/basango/apps/web/app/fonts/GeistMonoVF.woff b/basango/apps/web/app/fonts/GeistMonoVF.woff deleted file mode 100644 index f2ae185..0000000 Binary files a/basango/apps/web/app/fonts/GeistMonoVF.woff and /dev/null differ diff --git a/basango/apps/web/app/fonts/GeistVF.woff b/basango/apps/web/app/fonts/GeistVF.woff deleted file mode 100644 index 1b62daa..0000000 Binary files a/basango/apps/web/app/fonts/GeistVF.woff and /dev/null differ diff --git a/basango/apps/web/app/globals.css b/basango/apps/web/app/globals.css deleted file mode 100644 index 6af7ecb..0000000 --- a/basango/apps/web/app/globals.css +++ /dev/null @@ -1,50 +0,0 @@ -:root { - --background: #ffffff; - --foreground: #171717; -} - -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } -} - -html, -body { - max-width: 100vw; - overflow-x: hidden; -} - -body { - color: var(--foreground); - background: var(--background); -} - -* { - box-sizing: border-box; - padding: 0; - margin: 0; -} - -a { - color: inherit; - text-decoration: none; -} - -.imgDark { - display: none; -} - -@media (prefers-color-scheme: dark) { - html { - color-scheme: dark; - } - - .imgLight { - display: none; - } - .imgDark { - display: unset; - } -} diff --git a/basango/apps/web/app/layout.tsx b/basango/apps/web/app/layout.tsx deleted file mode 100644 index 8469537..0000000 --- a/basango/apps/web/app/layout.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import type { Metadata } from "next"; -import localFont from "next/font/local"; -import "./globals.css"; - -const geistSans = localFont({ - src: "./fonts/GeistVF.woff", - variable: "--font-geist-sans", -}); -const geistMono = localFont({ - src: "./fonts/GeistMonoVF.woff", - variable: "--font-geist-mono", -}); - -export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - - - {children} - - - ); -} diff --git a/basango/apps/web/app/page.module.css b/basango/apps/web/app/page.module.css deleted file mode 100644 index 3630662..0000000 --- a/basango/apps/web/app/page.module.css +++ /dev/null @@ -1,188 +0,0 @@ -.page { - --gray-rgb: 0, 0, 0; - --gray-alpha-200: rgba(var(--gray-rgb), 0.08); - --gray-alpha-100: rgba(var(--gray-rgb), 0.05); - - --button-primary-hover: #383838; - --button-secondary-hover: #f2f2f2; - - display: grid; - grid-template-rows: 20px 1fr 20px; - align-items: center; - justify-items: center; - min-height: 100svh; - padding: 80px; - gap: 64px; - font-synthesis: none; -} - -@media (prefers-color-scheme: dark) { - .page { - --gray-rgb: 255, 255, 255; - --gray-alpha-200: rgba(var(--gray-rgb), 0.145); - --gray-alpha-100: rgba(var(--gray-rgb), 0.06); - - --button-primary-hover: #ccc; - --button-secondary-hover: #1a1a1a; - } -} - -.main { - display: flex; - flex-direction: column; - gap: 32px; - grid-row-start: 2; -} - -.main ol { - font-family: var(--font-geist-mono); - padding-left: 0; - margin: 0; - font-size: 14px; - line-height: 24px; - letter-spacing: -0.01em; - list-style-position: inside; -} - -.main li:not(:last-of-type) { - margin-bottom: 8px; -} - -.main code { - font-family: inherit; - background: var(--gray-alpha-100); - padding: 2px 4px; - border-radius: 4px; - font-weight: 600; -} - -.ctas { - display: flex; - gap: 16px; -} - -.ctas a { - appearance: none; - border-radius: 128px; - height: 48px; - padding: 0 20px; - border: none; - font-family: var(--font-geist-sans); - border: 1px solid transparent; - transition: background 0.2s, color 0.2s, border-color 0.2s; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - font-size: 16px; - line-height: 20px; - font-weight: 500; -} - -a.primary { - background: var(--foreground); - color: var(--background); - gap: 8px; -} - -a.secondary { - border-color: var(--gray-alpha-200); - min-width: 180px; -} - -button.secondary { - appearance: none; - border-radius: 128px; - height: 48px; - padding: 0 20px; - border: none; - font-family: var(--font-geist-sans); - border: 1px solid transparent; - transition: background 0.2s, color 0.2s, border-color 0.2s; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - font-size: 16px; - line-height: 20px; - font-weight: 500; - background: transparent; - border-color: var(--gray-alpha-200); - min-width: 180px; -} - -.footer { - font-family: var(--font-geist-sans); - grid-row-start: 3; - display: flex; - gap: 24px; -} - -.footer a { - display: flex; - align-items: center; - gap: 8px; -} - -.footer img { - flex-shrink: 0; -} - -/* Enable hover only on non-touch devices */ -@media (hover: hover) and (pointer: fine) { - a.primary:hover { - background: var(--button-primary-hover); - border-color: transparent; - } - - a.secondary:hover { - background: var(--button-secondary-hover); - border-color: transparent; - } - - .footer a:hover { - text-decoration: underline; - text-underline-offset: 4px; - } -} - -@media (max-width: 600px) { - .page { - padding: 32px; - padding-bottom: 80px; - } - - .main { - align-items: center; - } - - .main ol { - text-align: center; - } - - .ctas { - flex-direction: column; - } - - .ctas a { - font-size: 14px; - height: 40px; - padding: 0 16px; - } - - a.secondary { - min-width: auto; - } - - .footer { - flex-wrap: wrap; - align-items: center; - justify-content: center; - } -} - -@media (prefers-color-scheme: dark) { - .logo { - filter: invert(); - } -} diff --git a/basango/apps/web/app/page.tsx b/basango/apps/web/app/page.tsx deleted file mode 100644 index 1fee7e2..0000000 --- a/basango/apps/web/app/page.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import Image, { type ImageProps } from "next/image"; -import { Button } from "@repo/ui/button"; -import styles from "./page.module.css"; - -type Props = Omit & { - srcLight: string; - srcDark: string; -}; - -const ThemeImage = (props: Props) => { - const { srcLight, srcDark, ...rest } = props; - - return ( - <> - - - - ); -}; - -export default function Home() { - return ( -
-
- -
    -
  1. - Get started by editing apps/web/app/page.tsx -
  2. -
  3. Save and see your changes instantly.
  4. -
- - - -
- -
- ); -} diff --git a/basango/apps/web/eslint.config.js b/basango/apps/web/eslint.config.js deleted file mode 100644 index 47b0670..0000000 --- a/basango/apps/web/eslint.config.js +++ /dev/null @@ -1,4 +0,0 @@ -import { nextJsConfig } from "@repo/eslint-config/next-js"; - -/** @type {import("eslint").Linter.Config[]} */ -export default nextJsConfig; diff --git a/basango/apps/web/next.config.js b/basango/apps/web/next.config.js deleted file mode 100644 index 4678774..0000000 --- a/basango/apps/web/next.config.js +++ /dev/null @@ -1,4 +0,0 @@ -/** @type {import('next').NextConfig} */ -const nextConfig = {}; - -export default nextConfig; diff --git a/basango/apps/web/package.json b/basango/apps/web/package.json deleted file mode 100644 index 5210659..0000000 --- a/basango/apps/web/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "web", - "version": "0.1.0", - "type": "module", - "private": true, - "scripts": { - "dev": "next dev --port 3000", - "build": "next build", - "start": "next start", - "lint": "eslint --max-warnings 0", - "check-types": "next typegen && tsc --noEmit" - }, - "dependencies": { - "@repo/ui": "*", - "next": "^16.0.0", - "react": "^19.2.0", - "react-dom": "^19.1.0" - }, - "devDependencies": { - "@repo/eslint-config": "*", - "@repo/typescript-config": "*", - "@types/node": "^22.15.3", - "@types/react": "19.1.0", - "@types/react-dom": "19.1.1", - "eslint": "^9.38.0", - "typescript": "5.9.2" - } -} diff --git a/basango/apps/web/public/file-text.svg b/basango/apps/web/public/file-text.svg deleted file mode 100644 index 9cfb3c9..0000000 --- a/basango/apps/web/public/file-text.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/basango/apps/web/public/globe.svg b/basango/apps/web/public/globe.svg deleted file mode 100644 index 4230a3d..0000000 --- a/basango/apps/web/public/globe.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/basango/apps/web/public/next.svg b/basango/apps/web/public/next.svg deleted file mode 100644 index 5174b28..0000000 --- a/basango/apps/web/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/basango/apps/web/public/turborepo-dark.svg b/basango/apps/web/public/turborepo-dark.svg deleted file mode 100644 index dae38fe..0000000 --- a/basango/apps/web/public/turborepo-dark.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/basango/apps/web/public/turborepo-light.svg b/basango/apps/web/public/turborepo-light.svg deleted file mode 100644 index ddea915..0000000 --- a/basango/apps/web/public/turborepo-light.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/basango/apps/web/public/vercel.svg b/basango/apps/web/public/vercel.svg deleted file mode 100644 index 0164ddc..0000000 --- a/basango/apps/web/public/vercel.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/basango/apps/web/public/window.svg b/basango/apps/web/public/window.svg deleted file mode 100644 index bbc7800..0000000 --- a/basango/apps/web/public/window.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/basango/apps/web/tsconfig.json b/basango/apps/web/tsconfig.json deleted file mode 100644 index 7aef056..0000000 --- a/basango/apps/web/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "extends": "@repo/typescript-config/nextjs.json", - "compilerOptions": { - "plugins": [ - { - "name": "next" - } - ] - }, - "include": [ - "**/*.ts", - "**/*.tsx", - "next-env.d.ts", - "next.config.js", - ".next/types/**/*.ts" - ], - "exclude": [ - "node_modules" - ] -} diff --git a/basango/biome.json b/basango/biome.json new file mode 100644 index 0000000..39e5600 --- /dev/null +++ b/basango/biome.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.3.1/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": false + }, + "formatter": { + "enabled": true, + "indentStyle": "space" + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" + } + } + } +} diff --git a/basango/bun.lock b/basango/bun.lock index 141f12d..3cc9cf7 100644 --- a/basango/bun.lock +++ b/basango/bun.lock @@ -4,636 +4,370 @@ "": { "name": "basango", "devDependencies": { - "prettier": "^3.6.2", + "@biomejs/biome": "^2.3.1", + "@manypkg/cli": "^0.25.1", "turbo": "^2.5.8", "typescript": "5.9.2", }, }, - "apps/docs": { - "name": "docs", + "apps/crawler": { + "name": "@basango/crawler", "version": "0.1.0", "dependencies": { - "@repo/ui": "*", - "next": "^16.0.0", - "react": "^19.2.0", - "react-dom": "^19.1.0", - }, - "devDependencies": { - "@repo/eslint-config": "*", - "@repo/typescript-config": "*", - "@types/node": "^22.15.3", - "@types/react": "19.1.0", - "@types/react-dom": "19.1.1", - "eslint": "^9.38.0", - "typescript": "5.9.2", + "bullmq": "^4.17.0", + "date-fns": "^3.6.0", + "ioredis": "^5.3.2", + "tiktoken": "^1.0.14", + "zod": "^4.0.0", }, }, - "apps/web": { - "name": "web", - "version": "0.1.0", + "packages/db": { + "name": "@basango/db", + "version": "1.0.0", "dependencies": { - "@repo/ui": "*", - "next": "^16.0.0", - "react": "^19.2.0", - "react-dom": "^19.1.0", + "@date-fns/utc": "^2.1.1", + "drizzle-orm": "^0.44.7", + "pg": "^8.16.3", + "snakecase-keys": "^9.0.2", }, "devDependencies": { - "@repo/eslint-config": "*", - "@repo/typescript-config": "*", - "@types/node": "^22.15.3", - "@types/react": "19.1.0", - "@types/react-dom": "19.1.1", - "eslint": "^9.38.0", - "typescript": "5.9.2", + "drizzle-kit": "^0.31.6", }, }, - "packages/eslint-config": { - "name": "@repo/eslint-config", + "packages/logger": { + "name": "@midday/logger", "version": "0.0.0", + "dependencies": { + "pino": "^10.1.0", + "pino-pretty": "^13.1.2", + }, "devDependencies": { - "@eslint/js": "^9.34.0", - "@next/eslint-plugin-next": "^15.5.0", - "eslint": "^9.38.0", - "eslint-config-prettier": "^10.1.1", - "eslint-plugin-only-warn": "^1.1.0", - "eslint-plugin-react": "^7.37.5", - "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-turbo": "^2.5.0", - "globals": "^16.3.0", "typescript": "^5.9.2", - "typescript-eslint": "^8.40.0", }, }, - "packages/typescript-config": { - "name": "@repo/typescript-config", + "packages/tsconfig": { + "name": "@basango/tsconfig", "version": "0.0.0", }, - "packages/ui": { - "name": "@repo/ui", - "version": "0.0.0", - "dependencies": { - "react": "^19.2.0", - "react-dom": "^19.1.0", - }, - "devDependencies": { - "@repo/eslint-config": "*", - "@repo/typescript-config": "*", - "@types/node": "^22.15.3", - "@types/react": "19.1.0", - "@types/react-dom": "19.1.1", - "eslint": "^9.38.0", - "typescript": "5.9.2", - }, - }, }, "packages": { - "@emnapi/runtime": ["@emnapi/runtime@1.6.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA=="], + "@basango/crawler": ["@basango/crawler@workspace:apps/crawler"], - "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="], + "@basango/db": ["@basango/db@workspace:packages/db"], - "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + "@basango/tsconfig": ["@basango/tsconfig@workspace:packages/tsconfig"], - "@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="], + "@biomejs/biome": ["@biomejs/biome@2.3.1", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.1", "@biomejs/cli-darwin-x64": "2.3.1", "@biomejs/cli-linux-arm64": "2.3.1", "@biomejs/cli-linux-arm64-musl": "2.3.1", "@biomejs/cli-linux-x64": "2.3.1", "@biomejs/cli-linux-x64-musl": "2.3.1", "@biomejs/cli-win32-arm64": "2.3.1", "@biomejs/cli-win32-x64": "2.3.1" }, "bin": { "biome": "bin/biome" } }, "sha512-A29evf1R72V5bo4o2EPxYMm5mtyGvzp2g+biZvRFx29nWebGyyeOSsDWGx3tuNNMFRepGwxmA9ZQ15mzfabK2w=="], - "@eslint/config-helpers": ["@eslint/config-helpers@0.4.1", "", { "dependencies": { "@eslint/core": "^0.16.0" } }, "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ombSf3MnTUueiYGN1SeI9tBCsDUhpWzOwS63Dove42osNh0PfE1cUtHFx6eZ1+MYCCLwXzlFlYFdrJ+U7h6LcA=="], - "@eslint/core": ["@eslint/core@0.16.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-pcOfwyoQkrkbGvXxRvZNe5qgD797IowpJPovPX5biPk2FwMEV+INZqfCaz4G5bVq9hYnjwhRMamg11U4QsRXrQ=="], - "@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-td5O8pFIgLs8H1sAZsD6v+5quODihyEw4nv2R8z7swUfIK1FKk+15e4eiYVLcAE4jUqngvh4j3JCNgg0Y4o4IQ=="], - "@eslint/js": ["@eslint/js@9.38.0", "", {}, "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-+DZYv8l7FlUtTrWs1Tdt1KcNCAmRO87PyOnxKGunbWm5HKg1oZBSbIIPkjrCtDZaeqSG1DiGx7qF+CPsquQRcg=="], - "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-PYWgEO7up7XYwSAArOpzsVCiqxBCXy53gsReAb1kKYIyXaoAlhBaBMvxR/k2Rm9aTuZ662locXUmPk/Aj+Xu+Q=="], - "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.0", "", { "dependencies": { "@eslint/core": "^0.16.0", "levn": "^0.4.1" } }, "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-Y3Ob4nqgv38Mh+6EGHltuN+Cq8aj/gyMTJYzkFZV2AEj+9XzoXB9VNljz9pjfFNHUxvLEV4b55VWyxozQTBaUQ=="], - "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-RHIG/zgo+69idUqVvV3n8+j58dKYABRpMyDmfWu2TITC+jwGPiEaT0Q3RKD+kQHiS80mpBrST0iUGeEXT0bU9A=="], - "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.1", "", { "os": "win32", "cpu": "x64" }, "sha512-izl30JJ5Dp10mi90Eko47zhxE6pYyWPcnX1NQxKpL/yMhXxf95oLTzfpu4q+MDBh/gemNqyJEwjBpe0MT5iWPA=="], - "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + "@date-fns/utc": ["@date-fns/utc@2.1.1", "", {}, "sha512-SlJDfG6RPeEX8wEVv6ZB3kak4MmbtyiI2qX/5zuKdordbrhB/iaJ58GVMZgJ6P1sJaM1gMgENFYYeg1JWrCFrA=="], - "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], - "@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="], + "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], - "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.3" }, "os": "darwin", "cpu": "arm64" }, "sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA=="], + "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], - "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.3" }, "os": "darwin", "cpu": "x64" }, "sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.11", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg=="], - "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw=="], + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.11", "", { "os": "android", "cpu": "arm" }, "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg=="], - "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA=="], + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.11", "", { "os": "android", "cpu": "arm64" }, "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ=="], - "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.3", "", { "os": "linux", "cpu": "arm" }, "sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA=="], + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.11", "", { "os": "android", "cpu": "x64" }, "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g=="], - "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ=="], + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w=="], - "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg=="], + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ=="], - "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w=="], + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.11", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA=="], - "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg=="], + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.11", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw=="], - "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw=="], + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.11", "", { "os": "linux", "cpu": "arm" }, "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw=="], - "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g=="], + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA=="], - "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.3" }, "os": "linux", "cpu": "arm" }, "sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA=="], + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.11", "", { "os": "linux", "cpu": "ia32" }, "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw=="], - "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.3" }, "os": "linux", "cpu": "arm64" }, "sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ=="], + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.11", "", { "os": "linux", "cpu": "none" }, "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw=="], - "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.3" }, "os": "linux", "cpu": "ppc64" }, "sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ=="], + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.11", "", { "os": "linux", "cpu": "none" }, "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ=="], - "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.3" }, "os": "linux", "cpu": "s390x" }, "sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw=="], + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.11", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw=="], - "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.3" }, "os": "linux", "cpu": "x64" }, "sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A=="], + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.11", "", { "os": "linux", "cpu": "none" }, "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww=="], - "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.3" }, "os": "linux", "cpu": "arm64" }, "sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA=="], + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.11", "", { "os": "linux", "cpu": "s390x" }, "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw=="], - "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.3" }, "os": "linux", "cpu": "x64" }, "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg=="], + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.11", "", { "os": "linux", "cpu": "x64" }, "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ=="], - "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.4", "", { "dependencies": { "@emnapi/runtime": "^1.5.0" }, "cpu": "none" }, "sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA=="], + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.11", "", { "os": "none", "cpu": "arm64" }, "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg=="], - "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA=="], + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.11", "", { "os": "none", "cpu": "x64" }, "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A=="], - "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw=="], + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.11", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg=="], - "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.4", "", { "os": "win32", "cpu": "x64" }, "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig=="], + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.11", "", { "os": "openbsd", "cpu": "x64" }, "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw=="], - "@next/env": ["@next/env@16.0.0", "", {}, "sha512-s5j2iFGp38QsG1LWRQaE2iUY3h1jc014/melHFfLdrsMJPqxqDQwWNwyQTcNoUSGZlCVZuM7t7JDMmSyRilsnA=="], + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.11", "", { "os": "none", "cpu": "arm64" }, "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ=="], - "@next/eslint-plugin-next": ["@next/eslint-plugin-next@15.5.6", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-YxDvsT2fwy1j5gMqk3ppXlsgDopHnkM4BoxSVASbvvgh5zgsK8lvWerDzPip8k3WVzsTZ1O7A7si1KNfN4OZfQ=="], + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.11", "", { "os": "sunos", "cpu": "x64" }, "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA=="], - "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.0.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/CntqDCnk5w2qIwMiF0a9r6+9qunZzFmU0cBX4T82LOflE72zzH6gnOjCwUXYKOBlQi8OpP/rMj8cBIr18x4TA=="], + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q=="], - "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.0.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-hB4GZnJGKa8m4efvTGNyii6qs76vTNl+3dKHTCAUaksN6KjYy4iEO3Q5ira405NW2PKb3EcqWiRaL9DrYJfMHg=="], + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.11", "", { "os": "win32", "cpu": "ia32" }, "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA=="], - "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.0.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-E2IHMdE+C1k+nUgndM13/BY/iJY9KGCphCftMh7SXWcaQqExq/pJU/1Hgn8n/tFwSoLoYC/yUghOv97tAsIxqg=="], + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.11", "", { "os": "win32", "cpu": "x64" }, "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA=="], - "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.0.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-xzgl7c7BVk4+7PDWldU+On2nlwnGgFqJ1siWp3/8S0KBBLCjonB6zwJYPtl4MUY7YZJrzzumdUpUoquu5zk8vg=="], + "@ioredis/commands": ["@ioredis/commands@1.4.0", "", {}, "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ=="], - "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.0.0", "", { "os": "linux", "cpu": "x64" }, "sha512-sdyOg4cbiCw7YUr0F/7ya42oiVBXLD21EYkSwN+PhE4csJH4MSXUsYyslliiiBwkM+KsuQH/y9wuxVz6s7Nstg=="], + "@manypkg/cli": ["@manypkg/cli@0.25.1", "", { "dependencies": { "@manypkg/get-packages": "^3.1.0", "detect-indent": "^7.0.1", "normalize-path": "^3.0.0", "p-limit": "^6.2.0", "package-json": "^10.0.1", "parse-github-url": "^1.0.3", "picocolors": "^1.1.1", "sembear": "^0.7.0", "semver": "^7.7.1", "tinyexec": "^1.0.1", "validate-npm-package-name": "^6.0.0" }, "bin": { "manypkg": "bin.js" } }, "sha512-lag906FyiNxzZjsRErkUD5/to174I2JzPk5bZubuJp6loMKKJn73zrtqeU7nHlVkHBg3tgXDTJj22HxUDxLRXw=="], - "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.0.0", "", { "os": "linux", "cpu": "x64" }, "sha512-IAXv3OBYqVaNOgyd3kxR4L3msuhmSy1bcchPHxDOjypG33i2yDWvGBwFD94OuuTjjTt/7cuIKtAmoOOml6kfbg=="], + "@manypkg/find-root": ["@manypkg/find-root@3.1.0", "", { "dependencies": { "@manypkg/tools": "^2.1.0" } }, "sha512-BcSqCyKhBVZ5YkSzOiheMCV41kqAFptW6xGqYSTjkVTl9XQpr+pqHhwgGCOHQtjDCv7Is6EFyA14Sm5GVbVABA=="], - "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.0.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-bmo3ncIJKUS9PWK1JD9pEVv0yuvp1KPuOsyJTHXTv8KDrEmgV/K+U0C75rl9rhIaODcS7JEb6/7eJhdwXI0XmA=="], + "@manypkg/get-packages": ["@manypkg/get-packages@3.1.0", "", { "dependencies": { "@manypkg/find-root": "^3.1.0", "@manypkg/tools": "^2.1.0" } }, "sha512-0TbBVyvPrP7xGYBI/cP8UP+yl/z+HtbTttAD7FMAJgn/kXOTwh5/60TsqP9ZYY710forNfyV0N8P/IE/ujGZJg=="], - "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.0.0", "", { "os": "win32", "cpu": "x64" }, "sha512-O1cJbT+lZp+cTjYyZGiDwsOjO3UHHzSqobkPNipdlnnuPb1swfcuY6r3p8dsKU4hAIEO4cO67ZCfVVH/M1ETXA=="], + "@manypkg/tools": ["@manypkg/tools@2.1.0", "", { "dependencies": { "jju": "^1.4.0", "js-yaml": "^4.1.0", "tinyglobby": "^0.2.13" } }, "sha512-0FOIepYR4ugPYaHwK7hDeHDkfPOBVvayt9QpvRbi2LT/h2b0GaE/gM9Gag7fsnyYyNaTZ2IGyOuVg07IYepvYQ=="], - "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + "@midday/logger": ["@midday/logger@workspace:packages/logger"], - "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="], - "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw=="], - "@repo/eslint-config": ["@repo/eslint-config@workspace:packages/eslint-config"], + "@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3", "", { "os": "linux", "cpu": "arm" }, "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw=="], - "@repo/typescript-config": ["@repo/typescript-config@workspace:packages/typescript-config"], + "@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg=="], - "@repo/ui": ["@repo/ui@workspace:packages/ui"], + "@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg=="], - "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], + "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ=="], - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], - "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + "@pnpm/config.env-replace": ["@pnpm/config.env-replace@1.1.0", "", {}, "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w=="], - "@types/node": ["@types/node@22.18.12", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-BICHQ67iqxQGFSzfCFTT7MRQ5XcBjG5aeKh5Ok38UBbPe5fxTyE+aHFxwVrGyr8GNlqFMLKD1D3P2K/1ks8tog=="], + "@pnpm/network.ca-file": ["@pnpm/network.ca-file@1.0.2", "", { "dependencies": { "graceful-fs": "4.2.10" } }, "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA=="], - "@types/react": ["@types/react@19.1.0", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-UaicktuQI+9UKyA4njtDOGBD/67t8YEBt2xdfqu8+gP9hqPUPsiXlNPcpS2gVdjmis5GKPG3fCxbQLVgxsQZ8w=="], - - "@types/react-dom": ["@types/react-dom@19.1.1", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-jFf/woGTVTjUJsl2O7hcopJ1r0upqoq/vIOoCj0yLh3RIXxWcljlpuZ+vEBRXsymD1jhfeJrlyTy/S1UW+4y1w=="], - - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.46.2", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/type-utils": "8.46.2", "@typescript-eslint/utils": "8.46.2", "@typescript-eslint/visitor-keys": "8.46.2", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.46.2", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w=="], - - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.46.2", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", "@typescript-eslint/typescript-estree": "8.46.2", "@typescript-eslint/visitor-keys": "8.46.2", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g=="], - - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.46.2", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.46.2", "@typescript-eslint/types": "^8.46.2", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg=="], - - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.46.2", "", { "dependencies": { "@typescript-eslint/types": "8.46.2", "@typescript-eslint/visitor-keys": "8.46.2" } }, "sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA=="], - - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.46.2", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag=="], - - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.46.2", "", { "dependencies": { "@typescript-eslint/types": "8.46.2", "@typescript-eslint/typescript-estree": "8.46.2", "@typescript-eslint/utils": "8.46.2", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA=="], - - "@typescript-eslint/types": ["@typescript-eslint/types@8.46.2", "", {}, "sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ=="], - - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.46.2", "", { "dependencies": { "@typescript-eslint/project-service": "8.46.2", "@typescript-eslint/tsconfig-utils": "8.46.2", "@typescript-eslint/types": "8.46.2", "@typescript-eslint/visitor-keys": "8.46.2", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ=="], - - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.46.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", "@typescript-eslint/typescript-estree": "8.46.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg=="], - - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.46.2", "", { "dependencies": { "@typescript-eslint/types": "8.46.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w=="], - - "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], - - "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], - - "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], - - "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "@pnpm/npm-conf": ["@pnpm/npm-conf@2.3.1", "", { "dependencies": { "@pnpm/config.env-replace": "^1.1.0", "@pnpm/network.ca-file": "^1.0.1", "config-chain": "^1.1.11" } }, "sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw=="], "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], - - "array-includes": ["array-includes@3.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="], - - "array.prototype.findlast": ["array.prototype.findlast@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ=="], - - "array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="], - - "array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="], - - "array.prototype.tosorted": ["array.prototype.tosorted@1.1.4", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3", "es-errors": "^1.3.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA=="], - - "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], - - "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], - - "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], + "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], - "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], + "bullmq": ["bullmq@4.18.3", "", { "dependencies": { "cron-parser": "^4.6.0", "glob": "^8.0.3", "ioredis": "^5.3.2", "lodash": "^4.17.21", "msgpackr": "^1.6.2", "node-abort-controller": "^3.1.1", "semver": "^7.5.4", "tslib": "^2.0.0", "uuid": "^9.0.0" } }, "sha512-H8t9vhfHEbJDaXp7aalSTe+Do+tR1nvr+lsT+jQxLhy+FFfFj/0p4aYJzADTNLdEqltuxneLVxCGVg92GkQx4w=="], - "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + "change-case": ["change-case@5.4.4", "", {}, "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w=="], - "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + "cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="], - "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], - "caniuse-lite": ["caniuse-lite@1.0.30001751", "", {}, "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw=="], + "config-chain": ["config-chain@1.1.13", "", { "dependencies": { "ini": "^1.3.4", "proto-list": "~1.2.1" } }, "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ=="], - "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "cron-parser": ["cron-parser@4.9.0", "", { "dependencies": { "luxon": "^3.2.1" } }, "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q=="], - "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], + "date-fns": ["date-fns@3.6.0", "", {}, "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww=="], - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - - "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], - - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - - "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], - - "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], - - "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="], - - "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], + "dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], - "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], - "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + "detect-indent": ["detect-indent@7.0.2", "", {}, "sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A=="], "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "docs": ["docs@workspace:apps/docs"], + "drizzle-kit": ["drizzle-kit@0.31.6", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-/B4e/4pwnx25QwD5xXgdpo1S+077a2VZdosXbItE/oNmUgQwZydGDz9qJYmnQl/b+5IX0rLfwRhrPnroGtrg8Q=="], - "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], + "drizzle-orm": ["drizzle-orm@0.44.7", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ=="], - "dotenv": ["dotenv@16.0.3", "", {}, "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ=="], + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], - "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + "esbuild": ["esbuild@0.25.11", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.11", "@esbuild/android-arm": "0.25.11", "@esbuild/android-arm64": "0.25.11", "@esbuild/android-x64": "0.25.11", "@esbuild/darwin-arm64": "0.25.11", "@esbuild/darwin-x64": "0.25.11", "@esbuild/freebsd-arm64": "0.25.11", "@esbuild/freebsd-x64": "0.25.11", "@esbuild/linux-arm": "0.25.11", "@esbuild/linux-arm64": "0.25.11", "@esbuild/linux-ia32": "0.25.11", "@esbuild/linux-loong64": "0.25.11", "@esbuild/linux-mips64el": "0.25.11", "@esbuild/linux-ppc64": "0.25.11", "@esbuild/linux-riscv64": "0.25.11", "@esbuild/linux-s390x": "0.25.11", "@esbuild/linux-x64": "0.25.11", "@esbuild/netbsd-arm64": "0.25.11", "@esbuild/netbsd-x64": "0.25.11", "@esbuild/openbsd-arm64": "0.25.11", "@esbuild/openbsd-x64": "0.25.11", "@esbuild/openharmony-arm64": "0.25.11", "@esbuild/sunos-x64": "0.25.11", "@esbuild/win32-arm64": "0.25.11", "@esbuild/win32-ia32": "0.25.11", "@esbuild/win32-x64": "0.25.11" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q=="], - "es-abstract": ["es-abstract@1.24.0", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg=="], + "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], - "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + "fast-copy": ["fast-copy@3.0.2", "", {}, "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ=="], - "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], - "es-iterator-helpers": ["es-iterator-helpers@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.0.3", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.6", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.4", "safe-array-concat": "^1.1.3" } }, "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], - "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], - "es-shim-unscopables": ["es-shim-unscopables@1.1.0", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw=="], + "glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="], - "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], + "graceful-fs": ["graceful-fs@4.2.10", "", {}, "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="], - "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + "help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="], - "eslint": ["eslint@9.38.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.1", "@eslint/core": "^0.16.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.38.0", "@eslint/plugin-kit": "^0.4.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw=="], + "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], - "eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="], + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - "eslint-plugin-only-warn": ["eslint-plugin-only-warn@1.1.0", "", {}, "sha512-2tktqUAT+Q3hCAU0iSf4xAN1k9zOpjK5WO8104mB0rT/dGhOa09582HN5HlbxNbPRZ0THV7nLGvzugcNOSjzfA=="], + "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], - "eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="], + "ioredis": ["ioredis@5.8.2", "", { "dependencies": { "@ioredis/commands": "1.4.0", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q=="], - "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="], + "jju": ["jju@1.4.0", "", {}, "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA=="], - "eslint-plugin-turbo": ["eslint-plugin-turbo@2.5.8", "", { "dependencies": { "dotenv": "16.0.3" }, "peerDependencies": { "eslint": ">6.6.0", "turbo": ">2.0.0" } }, "sha512-bVjx4vTH0oTKIyQ7EGFAXnuhZMrKIfu17qlex/dps7eScPnGQLJ3r1/nFq80l8xA+8oYjsSirSQ2tXOKbz3kEw=="], - - "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], - - "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], - - "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], - - "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], - - "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], - - "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - - "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], - - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - - "fast-glob": ["fast-glob@3.3.1", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="], - - "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], - - "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], - - "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], - - "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], - - "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - - "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], - - "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], - - "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], - - "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], - - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - - "function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="], - - "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], - - "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], - - "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], - - "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - - "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], - - "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - - "globals": ["globals@16.4.0", "", {}, "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw=="], - - "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], - - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], - - "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], - - "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], - - "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - - "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], - - "has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="], - - "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], - - "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], - - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - - "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - - "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], - - "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], - - "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], - - "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], - - "is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="], - - "is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="], - - "is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="], - - "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], - - "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], - - "is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="], - - "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="], - - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], - - "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], - - "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="], - - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - - "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], - - "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], - - "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], - - "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], - - "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], - - "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="], - - "is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="], - - "is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="], - - "is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="], - - "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], - - "is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="], - - "is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="], - - "is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="], - - "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], - - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - - "iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="], - - "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], - "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + "ky": ["ky@1.13.0", "", {}, "sha512-JeNNGs44hVUp2XxO3FY9WV28ymG7LgO4wju4HL/dCq1A8eKDcFgVrdCn1ssn+3Q/5OQilv5aYsL0DMt5mmAV9w=="], - "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], - "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + "lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="], - "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], + "lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="], - "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + "luxon": ["luxon@3.7.2", "", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="], - "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + "map-obj": ["map-obj@5.0.2", "", {}, "sha512-K6K2NgKnTXimT3779/4KxSvobxOtMmx1LBZ3NwRxT/MDIR3Br/fQ4Q+WCX5QxjyUR8zg5+RV9Tbf2c5pAWTD2A=="], - "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + "minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], - "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], - - "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], - - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - - "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], - - "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - - "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "msgpackr": ["msgpackr@1.11.5", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA=="], - "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + "msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="], - "next": ["next@16.0.0", "", { "dependencies": { "@next/env": "16.0.0", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.0.0", "@next/swc-darwin-x64": "16.0.0", "@next/swc-linux-arm64-gnu": "16.0.0", "@next/swc-linux-arm64-musl": "16.0.0", "@next/swc-linux-x64-gnu": "16.0.0", "@next/swc-linux-x64-musl": "16.0.0", "@next/swc-win32-arm64-msvc": "16.0.0", "@next/swc-win32-x64-msvc": "16.0.0", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-nYohiNdxGu4OmBzggxy9rczmjIGI+TpR5vbKTsE1HqYwNm1B+YSiugSrFguX6omMOKnDHAmBPY4+8TNJk0Idyg=="], + "node-abort-controller": ["node-abort-controller@3.1.1", "", {}, "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ=="], - "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], - "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], - "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], + "on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="], - "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - "object.entries": ["object.entries@1.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.1" } }, "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw=="], + "p-limit": ["p-limit@6.2.0", "", { "dependencies": { "yocto-queue": "^1.1.1" } }, "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA=="], - "object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="], + "package-json": ["package-json@10.0.1", "", { "dependencies": { "ky": "^1.2.0", "registry-auth-token": "^5.0.2", "registry-url": "^6.0.1", "semver": "^7.6.0" } }, "sha512-ua1L4OgXSBdsu1FPb7F3tYH0F48a6kxvod4pLUlGY9COeJAJQNX/sNH2IiEmsxw7lqYiAwrdHMjz1FctOsyDQg=="], - "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], + "parse-github-url": ["parse-github-url@1.0.3", "", { "bin": { "parse-github-url": "cli.js" } }, "sha512-tfalY5/4SqGaV/GIGzWyHnFjlpTPTNpENR9Ea2lLldSJ8EWXMsvacWucqY3m3I4YPtas15IxTLQVQ5NSYXPrww=="], - "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + "pg": ["pg@8.16.3", "", { "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", "pg-protocol": "^1.10.3", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.2.7" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw=="], - "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], + "pg-cloudflare": ["pg-cloudflare@1.2.7", "", {}, "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg=="], - "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + "pg-connection-string": ["pg-connection-string@2.9.1", "", {}, "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w=="], - "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], - "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + "pg-pool": ["pg-pool@3.10.1", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg=="], - "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + "pg-protocol": ["pg-protocol@1.10.3", "", {}, "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ=="], - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], - "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + "pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], + "pino": ["pino@10.1.0", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w=="], - "postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], + "pino-abstract-transport": ["pino-abstract-transport@2.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="], - "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + "pino-pretty": ["pino-pretty@13.1.2", "", { "dependencies": { "colorette": "^2.0.7", "dateformat": "^4.6.3", "fast-copy": "^3.0.2", "fast-safe-stringify": "^2.1.1", "help-me": "^5.0.0", "joycon": "^3.1.1", "minimist": "^1.2.6", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pump": "^3.0.0", "secure-json-parse": "^4.0.0", "sonic-boom": "^4.0.1", "strip-json-comments": "^5.0.2" }, "bin": { "pino-pretty": "bin.js" } }, "sha512-3cN0tCakkT4f3zo9RXDIhy6GTvtYD6bK4CRBLN9j3E/ePqN1tugAXD5rGVfoChW6s0hiek+eyYlLNqc/BG7vBQ=="], - "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], + "pino-std-serializers": ["pino-std-serializers@7.0.0", "", {}, "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA=="], - "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], + "postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], - "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "postgres-bytea": ["postgres-bytea@1.0.0", "", {}, "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="], - "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + "postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="], - "react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="], + "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], - "react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="], + "process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="], - "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + "proto-list": ["proto-list@1.2.4", "", {}, "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA=="], - "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], + "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], - "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], + "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], - "resolve": ["resolve@2.0.0-next.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="], + "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], - "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], - "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + "redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="], - "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + "redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="], - "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], + "registry-auth-token": ["registry-auth-token@5.1.0", "", { "dependencies": { "@pnpm/npm-conf": "^2.1.0" } }, "sha512-GdekYuwLXLxMuFTwAPg5UKGLW/UXzQrZvH/Zj791BQif5T05T0RsaLfHc9q3ZOKi7n+BoprPD9mJ0O0k4xzUlw=="], - "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="], + "registry-url": ["registry-url@6.0.1", "", { "dependencies": { "rc": "1.2.8" } }, "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q=="], - "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], - "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], - "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "secure-json-parse": ["secure-json-parse@4.1.0", "", {}, "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA=="], - "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], + "sembear": ["sembear@0.7.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-XyLTEich2D02FODCkfdto3mB9DetWPLuTzr4tvoofe9SvyM27h4nQSbV3+iVcYQz94AFyKtqBv5pcZbj3k2hdA=="], - "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], + "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], + "snakecase-keys": ["snakecase-keys@9.0.2", "", { "dependencies": { "change-case": "^5.4.4", "map-obj": "^5.0.2", "type-fest": "^4.15.0" } }, "sha512-Tr4gONsDj1Pa6HJH9D3b411r6tuRyCGgb1l7YpzDFp/thjVSWs7rcbNjyTyRqJi5SUV23sFpzf9epIJRbLR6Yw=="], - "sharp": ["sharp@0.34.4", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.0", "semver": "^7.7.2" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.4", "@img/sharp-darwin-x64": "0.34.4", "@img/sharp-libvips-darwin-arm64": "1.2.3", "@img/sharp-libvips-darwin-x64": "1.2.3", "@img/sharp-libvips-linux-arm": "1.2.3", "@img/sharp-libvips-linux-arm64": "1.2.3", "@img/sharp-libvips-linux-ppc64": "1.2.3", "@img/sharp-libvips-linux-s390x": "1.2.3", "@img/sharp-libvips-linux-x64": "1.2.3", "@img/sharp-libvips-linuxmusl-arm64": "1.2.3", "@img/sharp-libvips-linuxmusl-x64": "1.2.3", "@img/sharp-linux-arm": "0.34.4", "@img/sharp-linux-arm64": "0.34.4", "@img/sharp-linux-ppc64": "0.34.4", "@img/sharp-linux-s390x": "0.34.4", "@img/sharp-linux-x64": "0.34.4", "@img/sharp-linuxmusl-arm64": "0.34.4", "@img/sharp-linuxmusl-x64": "0.34.4", "@img/sharp-wasm32": "0.34.4", "@img/sharp-win32-arm64": "0.34.4", "@img/sharp-win32-ia32": "0.34.4", "@img/sharp-win32-x64": "0.34.4" } }, "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA=="], + "sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="], - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], - "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], - "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + "standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="], - "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + "strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="], - "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + "thread-stream": ["thread-stream@3.1.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="], - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "tiktoken": ["tiktoken@1.0.22", "", {}, "sha512-PKvy1rVF1RibfF3JlXBSP0Jrcw2uq3yXdgcEXtKTYn3QJ/cBRBHDnrJ5jHky+MENZ6DIPwNUGWpkVx+7joCpNA=="], - "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], + "tinyexec": ["tinyexec@1.0.1", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="], - "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="], - - "string.prototype.repeat": ["string.prototype.repeat@1.0.0", "", { "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" } }, "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w=="], - - "string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="], - - "string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="], - - "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], - - "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], - - "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], - - "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - - "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - - "ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="], + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -651,60 +385,68 @@ "turbo-windows-arm64": ["turbo-windows-arm64@2.5.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-eFC5XzLmgXJfnAK3UMTmVECCwuBcORrWdewoiXBnUm934DY6QN8YowC/srhNnROMpaKaqNeRpoB5FxCww3eteQ=="], - "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], - - "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], - - "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="], - - "typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="], - - "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], + "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], - "typescript-eslint": ["typescript-eslint@8.46.2", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.46.2", "@typescript-eslint/parser": "8.46.2", "@typescript-eslint/typescript-estree": "8.46.2", "@typescript-eslint/utils": "8.46.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-vbw8bOmiuYNdzzV3lsiWv6sRwjyuKJMQqWulBOU7M0RrxedXledX8G8kBbQeiOYDnTfiXz0Y4081E1QMNB6iQg=="], + "uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], - "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], + "validate-npm-package-name": ["validate-npm-package-name@6.0.2", "", {}, "sha512-IUoow1YUtvoBBC06dXs8bR8B9vuA3aJfmQNKMoaPG/OFsPmoQvw8xh+6Ye25Gx9DQhoEom3Pcu9MKHerm/NpUQ=="], - "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], - "web": ["web@workspace:apps/web"], + "yocto-queue": ["yocto-queue@1.2.1", "", {}, "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg=="], - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "zod": ["zod@4.1.12", "", {}, "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ=="], - "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], + "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], - "which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="], + "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], - "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], - "which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], - "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="], - "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="], - "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="], - "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="], - "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="], - "@typescript-eslint/typescript-estree/fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="], - "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="], - "@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="], - "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="], - "sharp/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="], - "@typescript-eslint/typescript-estree/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], } } diff --git a/basango/package.json b/basango/package.json index e4cc8c1..fa8fa82 100644 --- a/basango/package.json +++ b/basango/package.json @@ -1,24 +1,28 @@ { - "name": "basango", - "private": true, - "scripts": { - "build": "turbo run build", - "dev": "turbo run dev", - "lint": "turbo run lint", - "format": "prettier --write \"**/*.{ts,tsx,md}\"", - "check-types": "turbo run check-types" - }, - "devDependencies": { - "prettier": "^3.6.2", - "turbo": "^2.5.8", - "typescript": "5.9.2" - }, - "engines": { - "node": ">=22" - }, - "packageManager": "bun@1.2.8", - "workspaces": [ - "apps/*", - "packages/*" - ] + "name": "basango", + "private": true, + "scripts": { + "build": "turbo run build", + "clean": "git clean -xdf node_modules", + "clean:workspaces": "turbo run clean", + "dev": "turbo run dev --parallel", + "test": "turbo run test --parallel", + "lint": "turbo run lint && manypkg check", + "format": "biome format --write .", + "typecheck": "turbo run typecheck" + }, + "devDependencies": { + "@biomejs/biome": "^2.3.1", + "@manypkg/cli": "^0.25.1", + "turbo": "^2.5.8", + "typescript": "5.9.2" + }, + "engines": { + "node": ">=22" + }, + "packageManager": "bun@1.2.8", + "workspaces": [ + "apps/*", + "packages/*" + ] } diff --git a/basango/packages/db/drizzle.config.ts b/basango/packages/db/drizzle.config.ts new file mode 100644 index 0000000..d853548 --- /dev/null +++ b/basango/packages/db/drizzle.config.ts @@ -0,0 +1,10 @@ +import type { Config } from "drizzle-kit"; + +export default { + schema: "./src/schema.ts", + out: "./migrations", + dialect: "postgresql", + dbCredentials: { + url: process.env.DATABASE_SESSION_POOLER!, + }, +} satisfies Config; \ No newline at end of file diff --git a/basango/packages/db/package.json b/basango/packages/db/package.json new file mode 100644 index 0000000..8408715 --- /dev/null +++ b/basango/packages/db/package.json @@ -0,0 +1,17 @@ +{ + "name": "@basango/db", + "version": "1.0.0", + "main": "index.ts", + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "@date-fns/utc": "^2.1.1", + "drizzle-orm": "^0.44.7", + "pg": "^8.16.3", + "snakecase-keys": "^9.0.2" + }, + "devDependencies": { + "drizzle-kit": "^0.31.6" + } +} diff --git a/basango/packages/db/src/client.ts b/basango/packages/db/src/client.ts new file mode 100644 index 0000000..f554d97 --- /dev/null +++ b/basango/packages/db/src/client.ts @@ -0,0 +1,59 @@ +import { drizzle } from "drizzle-orm/node-postgres"; +import { Pool } from "pg"; +import * as schema from "@basango/db/schema"; + +const isDevelopment = process.env.NODE_ENV === "development"; + +const connectionConfig = { + max: isDevelopment ? 8 : 12, + idleTimeoutMillis: isDevelopment ? 5_000 : 60_000, + connectionTimeoutMillis: 15_000, + maxUses: isDevelopment ? 100 : 0, + allowExitOnIdle: true, +}; + +const pool = new Pool({ + connectionString: + process.env.DATABASE_URL ?? process.env.DATABASE_PRIMARY_URL!, + ...connectionConfig, +}); + +// Lightweight connection pool monitoring (single pool) +export const getConnectionPoolStats = () => { + const stats = { + name: "primary", + total: pool.options.max ?? 0, + idle: pool.idleCount ?? 0, + active: Math.max(0, (pool.totalCount ?? 0) - (pool.idleCount ?? 0)), + waiting: pool.waitingCount ?? 0, + ended: (pool as any).ended ?? false, + }; + + const totalConnections = connectionConfig.max; + const utilization = + totalConnections > 0 + ? Math.round((stats.active / totalConnections) * 100) + : 0; + + return { + timestamp: new Date().toISOString(), + region: process.env.FLY_REGION || "unknown", + instance: process.env.FLY_ALLOC_ID || "local", + pools: { primary: stats }, + summary: { + totalConnections, + totalActive: stats.active, + totalWaiting: stats.waiting, + hasExhaustedPools: + stats.active >= totalConnections || (stats.waiting ?? 0) > 0, + utilizationPercent: utilization, + }, + }; +}; + +export const db = drizzle(pool, { + schema, + casing: "snake_case", +}); +export const connectDb = async () => db; +export type Database = Awaited>; diff --git a/basango/packages/db/src/queries/activities.ts b/basango/packages/db/src/queries/activities.ts new file mode 100644 index 0000000..debea05 --- /dev/null +++ b/basango/packages/db/src/queries/activities.ts @@ -0,0 +1,164 @@ +import type { Database } from "@db/client"; +import { activities } from "@db/schema"; +import type { activityStatusEnum, activityTypeEnum } from "@db/schema"; +import { and, desc, eq, inArray, lte, ne } from "drizzle-orm"; +import type { SQL } from "drizzle-orm/sql/sql"; + +type CreateActivityParams = { + teamId: string; + userId?: string; + type: (typeof activityTypeEnum.enumValues)[number]; + source: "system" | "user"; + status?: (typeof activityStatusEnum.enumValues)[number]; + priority?: number; + groupId?: string; + metadata: Record; +}; + +export async function createActivity( + db: Database, + params: CreateActivityParams, +) { + const [result] = await db + .insert(activities) + .values({ + teamId: params.teamId, + userId: params.userId, + type: params.type, + source: params.source, + status: params.status, + priority: params.priority ?? 5, + groupId: params.groupId, + metadata: params.metadata, + }) + .returning(); + + return result; +} + +export async function updateActivityStatus( + db: Database, + activityId: string, + status: (typeof activityStatusEnum.enumValues)[number], + teamId: string, +) { + const [result] = await db + .update(activities) + .set({ status }) + .where(and(eq(activities.id, activityId), eq(activities.teamId, teamId))) + .returning(); + + return result; +} + +export async function updateAllActivitiesStatus( + db: Database, + teamId: string, + status: (typeof activityStatusEnum.enumValues)[number], + options: { userId: string }, +) { + const conditions = [ + eq(activities.teamId, teamId), + eq(activities.userId, options.userId), + ]; + + // Only update specific statuses based on the target status + if (status === "archived") { + // When archiving, update unread and read notifications + conditions.push(inArray(activities.status, ["unread", "read"])); + } else if (status === "read") { + // When marking as read, only update unread notifications (never archived) + conditions.push(eq(activities.status, "unread")); + } else { + // For other statuses, use the original logic but exclude archived + conditions.push(ne(activities.status, status)); + conditions.push(ne(activities.status, "archived")); + } + + const result = await db + .update(activities) + .set({ status }) + .where(and(...conditions)) + .returning(); + + return result; +} + +export type GetActivitiesParams = { + teamId: string; + cursor?: string | null; + pageSize?: number; + status?: + | (typeof activityStatusEnum.enumValues)[number][] + | (typeof activityStatusEnum.enumValues)[number] + | null; + userId?: string | null; + priority?: number | null; + maxPriority?: number | null; // For filtering notifications (priority <= 3) +}; + +export async function getActivities(db: Database, params: GetActivitiesParams) { + const { + teamId, + cursor, + pageSize = 20, + status, + userId, + priority, + maxPriority, + } = params; + + // Convert cursor to offset + const offset = cursor ? Number.parseInt(cursor, 10) : 0; + + // Base conditions for the WHERE clause + const whereConditions: SQL[] = [eq(activities.teamId, teamId)]; + + // Filter by status - support both single status and array of statuses + if (status) { + if (Array.isArray(status)) { + whereConditions.push(inArray(activities.status, status)); + } else { + whereConditions.push(eq(activities.status, status)); + } + } + + // Filter by user if specified + if (userId) { + whereConditions.push(eq(activities.userId, userId)); + } + + // Filter by priority if specified + if (priority) { + whereConditions.push(eq(activities.priority, priority)); + } + + // Filter by max priority if specified (for notifications: priority <= 3) + if (maxPriority) { + whereConditions.push(lte(activities.priority, maxPriority)); + } + + // Execute the query with proper ordering and pagination + const data = await db + .select() + .from(activities) + .where(and(...whereConditions)) + .orderBy(desc(activities.createdAt)) // Most recent first + .limit(pageSize) + .offset(offset); + + // Calculate next cursor + const nextCursor = + data && data.length === pageSize + ? (offset + pageSize).toString() + : undefined; + + return { + meta: { + cursor: nextCursor ?? null, + hasPreviousPage: offset > 0, + hasNextPage: data && data.length === pageSize, + }, + data, + }; +} diff --git a/basango/packages/db/src/schema.ts b/basango/packages/db/src/schema.ts new file mode 100644 index 0000000..0179685 --- /dev/null +++ b/basango/packages/db/src/schema.ts @@ -0,0 +1,3269 @@ +import { type SQL, relations, sql } from "drizzle-orm"; +import { + bigint, + boolean, + customType, + date, + foreignKey, + index, + integer, + json, + jsonb, + numeric, + pgEnum, + pgMaterializedView, + pgPolicy, + pgTable, + primaryKey, + smallint, + text, + timestamp, + unique, + uniqueIndex, + uuid, + varchar, + vector, +} from "drizzle-orm/pg-core"; + +export const tsvector = customType<{ + data: string; +}>({ + dataType() { + return "tsvector"; + }, +}); + +type NumericConfig = { + precision?: number; + scale?: number; +}; + +export const numericCasted = customType<{ + data: number; + driverData: string; + config: NumericConfig; +}>({ + dataType: (config) => { + if (config?.precision && config?.scale) { + return `numeric(${config.precision}, ${config.scale})`; + } + return "numeric"; + }, + fromDriver: (value: string) => Number.parseFloat(value), + toDriver: (value: number) => value.toString(), +}); + +export const accountTypeEnum = pgEnum("account_type", [ + "depository", + "credit", + "other_asset", + "loan", + "other_liability", +]); + +export const bankProvidersEnum = pgEnum("bank_providers", [ + "gocardless", + "plaid", + "teller", + "enablebanking", +]); + +export const connectionStatusEnum = pgEnum("connection_status", [ + "disconnected", + "connected", + "unknown", +]); + +export const documentProcessingStatusEnum = pgEnum( + "document_processing_status", + ["pending", "processing", "completed", "failed"], +); + +export const inboxAccountProvidersEnum = pgEnum("inbox_account_providers", [ + "gmail", + "outlook", +]); + +export const inboxAccountStatusEnum = pgEnum("inbox_account_status", [ + "connected", + "disconnected", +]); + +export const inboxStatusEnum = pgEnum("inbox_status", [ + "processing", + "pending", + "archived", + "new", + "analyzing", + "suggested_match", + "no_match", + "done", + "deleted", +]); + +export const inboxTypeEnum = pgEnum("inbox_type", ["invoice", "expense"]); +export const invoiceDeliveryTypeEnum = pgEnum("invoice_delivery_type", [ + "create", + "create_and_send", + "scheduled", +]); + +export const invoiceSizeEnum = pgEnum("invoice_size", ["a4", "letter"]); +export const invoiceStatusEnum = pgEnum("invoice_status", [ + "draft", + "overdue", + "paid", + "unpaid", + "canceled", + "scheduled", +]); + +export const plansEnum = pgEnum("plans", ["trial", "starter", "pro"]); +export const subscriptionStatusEnum = pgEnum("subscription_status", [ + "active", + "canceled", + "past_due", + "unpaid", + "trialing", + "incomplete", + "incomplete_expired", +]); +export const reportTypesEnum = pgEnum("reportTypes", [ + "profit", + "revenue", + "burn_rate", + "expense", +]); + +export const teamRolesEnum = pgEnum("teamRoles", ["owner", "member"]); +export const trackerStatusEnum = pgEnum("trackerStatus", [ + "in_progress", + "completed", +]); + +export const transactionMethodsEnum = pgEnum("transactionMethods", [ + "payment", + "card_purchase", + "card_atm", + "transfer", + "other", + "unknown", + "ach", + "interest", + "deposit", + "wire", + "fee", +]); + +export const transactionStatusEnum = pgEnum("transactionStatus", [ + "posted", + "pending", + "excluded", + "completed", + "archived", +]); + +export const transactionFrequencyEnum = pgEnum("transaction_frequency", [ + "weekly", + "biweekly", + "monthly", + "semi_monthly", + "annually", + "irregular", + "unknown", +]); + +export const activityTypeEnum = pgEnum("activity_type", [ + // System-generated activities + "transactions_enriched", + "transactions_created", + "invoice_paid", + "inbox_new", + "inbox_auto_matched", + "inbox_needs_review", + "inbox_cross_currency_matched", + "invoice_overdue", + "invoice_sent", + "inbox_match_confirmed", + + // User actions + "document_uploaded", + "document_processed", + "invoice_duplicated", + "invoice_scheduled", + "invoice_reminder_sent", + "invoice_cancelled", + "invoice_created", + "draft_invoice_created", + "tracker_entry_created", + "tracker_project_created", + "transactions_categorized", + "transactions_assigned", + "transaction_attachment_created", + "transaction_category_created", + "transactions_exported", + "customer_created", +]); + +export const activitySourceEnum = pgEnum("activity_source", [ + "system", // Automated system processes + "user", // Direct user actions +]); + +export const activityStatusEnum = pgEnum("activity_status", [ + "unread", + "read", + "archived", +]); + +export const documentTagEmbeddings = pgTable( + "document_tag_embeddings", + { + slug: text().primaryKey().notNull(), + embedding: vector({ dimensions: 768 }), + name: text().notNull(), + model: text().notNull().default("gemini-embedding-001"), + }, + (table) => [ + index("document_tag_embeddings_idx") + .using("hnsw", table.embedding.asc().nullsLast().op("vector_cosine_ops")) + .with({ m: "16", ef_construction: "64" }), + pgPolicy("Enable insert for authenticated users only", { + as: "permissive", + for: "insert", + to: ["authenticated"], + withCheck: sql`true`, + }), + ], +); + +export const transactionCategoryEmbeddings = pgTable( + "transaction_category_embeddings", + { + name: text().primaryKey().notNull(), // Unique by name - same embedding for all teams + embedding: vector({ dimensions: 768 }), + model: text().notNull().default("gemini-embedding-001"), + system: boolean().default(false).notNull(), // Whether this comes from system categories + createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) + .defaultNow() + .notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true, mode: "string" }) + .defaultNow() + .notNull(), + }, + (table) => [ + // Vector similarity index for fast cosine similarity search + index("transaction_category_embeddings_vector_idx") + .using("hnsw", table.embedding.asc().nullsLast().op("vector_cosine_ops")) + .with({ m: "16", ef_construction: "64" }), + // System categories index for filtering + index("transaction_category_embeddings_system_idx").using( + "btree", + table.system.asc().nullsLast().op("bool_ops"), + ), + pgPolicy("Enable read access for authenticated users", { + as: "permissive", + for: "select", + to: ["authenticated"], + using: sql`true`, + }), + pgPolicy("Enable insert for authenticated users only", { + as: "permissive", + for: "insert", + to: ["authenticated"], + withCheck: sql`true`, + }), + pgPolicy("Enable update for authenticated users only", { + as: "permissive", + for: "update", + to: ["authenticated"], + using: sql`true`, + }), + ], +); + +export const transactions = pgTable( + "transactions", + { + id: uuid().defaultRandom().primaryKey().notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) + .defaultNow() + .notNull(), + date: date().notNull(), + name: text().notNull(), + method: transactionMethodsEnum().notNull(), + amount: numericCasted({ precision: 10, scale: 2 }).notNull(), + currency: text().notNull(), + teamId: uuid("team_id").notNull(), + assignedId: uuid("assigned_id"), + note: varchar(), + bankAccountId: uuid("bank_account_id"), + internalId: text("internal_id").notNull(), + status: transactionStatusEnum().default("posted"), + balance: numericCasted({ precision: 10, scale: 2 }), + manual: boolean().default(false), + notified: boolean().default(false), + internal: boolean().default(false), + description: text(), + categorySlug: text("category_slug"), + baseAmount: numericCasted({ precision: 10, scale: 2 }), + counterpartyName: text("counterparty_name"), + baseCurrency: text("base_currency"), + taxAmount: numericCasted("tax_amount", { precision: 10, scale: 2 }), + taxRate: numericCasted("tax_rate", { precision: 10, scale: 2 }), + taxType: text("tax_type"), + recurring: boolean(), + frequency: transactionFrequencyEnum(), + merchantName: text("merchant_name"), + enrichmentCompleted: boolean("enrichment_completed").default(false), + ftsVector: tsvector("fts_vector") + .notNull() + .generatedAlwaysAs( + (): SQL => sql` + to_tsvector( + 'english', + ( + (COALESCE(name, ''::text) || ' '::text) || COALESCE(description, ''::text) + ) + ) + `, + ), + }, + (table) => [ + index("idx_transactions_date").using( + "btree", + table.date.asc().nullsLast().op("date_ops"), + ), + index("idx_transactions_fts").using( + "gin", + table.ftsVector.asc().nullsLast().op("tsvector_ops"), + ), + index("idx_transactions_fts_vector").using( + "gin", + table.ftsVector.asc().nullsLast().op("tsvector_ops"), + ), + index("idx_transactions_id").using( + "btree", + table.id.asc().nullsLast().op("uuid_ops"), + ), + index("idx_transactions_name").using( + "btree", + table.name.asc().nullsLast().op("text_ops"), + ), + index("idx_transactions_name_trigram").using( + "gin", + table.name.asc().nullsLast().op("gin_trgm_ops"), + ), + index("idx_transactions_team_id_date_name").using( + "btree", + table.teamId.asc().nullsLast().op("date_ops"), + table.date.asc().nullsLast().op("date_ops"), + table.name.asc().nullsLast().op("uuid_ops"), + ), + index("idx_transactions_team_id_name").using( + "btree", + table.teamId.asc().nullsLast().op("uuid_ops"), + table.name.asc().nullsLast().op("uuid_ops"), + ), + index("idx_trgm_name").using( + "gist", + table.name.asc().nullsLast().op("gist_trgm_ops"), + ), + index("transactions_assigned_id_idx").using( + "btree", + table.assignedId.asc().nullsLast().op("uuid_ops"), + ), + index("transactions_bank_account_id_idx").using( + "btree", + table.bankAccountId.asc().nullsLast().op("uuid_ops"), + ), + index("transactions_category_slug_idx").using( + "btree", + table.categorySlug.asc().nullsLast().op("text_ops"), + ), + index( + "transactions_team_id_date_currency_bank_account_id_category_idx", + ).using( + "btree", + table.teamId.asc().nullsLast().op("enum_ops"), + table.date.asc().nullsLast().op("date_ops"), + table.currency.asc().nullsLast().op("text_ops"), + table.bankAccountId.asc().nullsLast().op("date_ops"), + ), + index("transactions_team_id_idx").using( + "btree", + table.teamId.asc().nullsLast().op("uuid_ops"), + ), + foreignKey({ + columns: [table.assignedId], + foreignColumns: [users.id], + name: "public_transactions_assigned_id_fkey", + }).onDelete("set null"), + foreignKey({ + columns: [table.teamId], + foreignColumns: [teams.id], + name: "public_transactions_team_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.bankAccountId], + foreignColumns: [bankAccounts.id], + name: "transactions_bank_account_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.teamId, table.categorySlug], + foreignColumns: [ + transactionCategories.teamId, + transactionCategories.slug, + ], + name: "transactions_category_slug_team_id_fkey", + }), + unique("transactions_internal_id_key").on(table.internalId), + pgPolicy("Transactions can be created by a member of the team", { + as: "permissive", + for: "insert", + to: ["public"], + withCheck: sql`(team_id IN ( SELECT private.get_teams_for_authenticated_user() AS get_teams_for_authenticated_user))`, + }), + pgPolicy("Transactions can be deleted by a member of the team", { + as: "permissive", + for: "delete", + to: ["public"], + }), + pgPolicy("Transactions can be selected by a member of the team", { + as: "permissive", + for: "select", + to: ["public"], + }), + pgPolicy("Transactions can be updated by a member of the team", { + as: "permissive", + for: "update", + to: ["public"], + }), + ], +); + +export const trackerEntries = pgTable( + "tracker_entries", + { + id: uuid().defaultRandom().primaryKey().notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) + .defaultNow() + .notNull(), + // You can use { mode: "bigint" } if numbers are exceeding js number limitations + duration: bigint({ mode: "number" }), + projectId: uuid("project_id"), + start: timestamp({ withTimezone: true, mode: "string" }), + stop: timestamp({ withTimezone: true, mode: "string" }), + assignedId: uuid("assigned_id"), + teamId: uuid("team_id"), + description: text(), + rate: numericCasted({ precision: 10, scale: 2 }), + currency: text(), + billed: boolean().default(false), + date: date().defaultNow(), + }, + (table) => [ + index("tracker_entries_team_id_idx").using( + "btree", + table.teamId.asc().nullsLast().op("uuid_ops"), + ), + foreignKey({ + columns: [table.assignedId], + foreignColumns: [users.id], + name: "tracker_entries_assigned_id_fkey", + }).onDelete("set null"), + foreignKey({ + columns: [table.projectId], + foreignColumns: [trackerProjects.id], + name: "tracker_entries_project_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.teamId], + foreignColumns: [teams.id], + name: "tracker_entries_team_id_fkey", + }).onDelete("cascade"), + pgPolicy("Entries can be created by a member of the team", { + as: "permissive", + for: "insert", + to: ["authenticated"], + withCheck: sql`(team_id IN ( SELECT private.get_teams_for_authenticated_user() AS get_teams_for_authenticated_user))`, + }), + pgPolicy("Entries can be deleted by a member of the team", { + as: "permissive", + for: "delete", + to: ["authenticated"], + }), + pgPolicy("Entries can be selected by a member of the team", { + as: "permissive", + for: "select", + to: ["authenticated"], + }), + pgPolicy("Entries can be updated by a member of the team", { + as: "permissive", + for: "update", + to: ["authenticated"], + }), + ], +); + +export const customerTags = pgTable( + "customer_tags", + { + id: uuid().defaultRandom().primaryKey().notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) + .defaultNow() + .notNull(), + customerId: uuid("customer_id").notNull(), + teamId: uuid("team_id").notNull(), + tagId: uuid("tag_id").notNull(), + }, + (table) => [ + foreignKey({ + columns: [table.customerId], + foreignColumns: [customers.id], + name: "customer_tags_customer_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.tagId], + foreignColumns: [tags.id], + name: "customer_tags_tag_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.teamId], + foreignColumns: [teams.id], + name: "customer_tags_team_id_fkey", + }).onDelete("cascade"), + unique("unique_customer_tag").on(table.customerId, table.tagId), + pgPolicy("Tags can be handled by a member of the team", { + as: "permissive", + for: "all", + to: ["public"], + using: sql`(team_id IN ( SELECT private.get_teams_for_authenticated_user() AS get_teams_for_authenticated_user))`, + }), + ], +); + +export const inboxAccounts = pgTable( + "inbox_accounts", + { + id: uuid().defaultRandom().primaryKey().notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) + .defaultNow() + .notNull(), + email: text().notNull(), + accessToken: text("access_token").notNull(), + refreshToken: text("refresh_token").notNull(), + teamId: uuid("team_id").notNull(), + lastAccessed: timestamp("last_accessed", { + withTimezone: true, + mode: "string", + }).notNull(), + provider: inboxAccountProvidersEnum().notNull(), + externalId: text("external_id").notNull(), + expiryDate: timestamp("expiry_date", { + withTimezone: true, + mode: "string", + }).notNull(), + scheduleId: text("schedule_id"), + status: inboxAccountStatusEnum().default("connected").notNull(), + errorMessage: text("error_message"), + }, + (table) => [ + foreignKey({ + columns: [table.teamId], + foreignColumns: [teams.id], + name: "inbox_accounts_team_id_fkey", + }).onDelete("cascade"), + unique("inbox_accounts_email_key").on(table.email), + unique("inbox_accounts_external_id_key").on(table.externalId), + pgPolicy("Inbox accounts can be deleted by a member of the team", { + as: "permissive", + for: "delete", + to: ["public"], + using: sql`(team_id IN ( SELECT private.get_teams_for_authenticated_user() AS get_teams_for_authenticated_user))`, + }), + pgPolicy("Inbox accounts can be selected by a member of the team", { + as: "permissive", + for: "select", + to: ["public"], + }), + pgPolicy("Inbox accounts can be updated by a member of the team", { + as: "permissive", + for: "update", + to: ["public"], + }), + ], +); + +export const bankAccounts = pgTable( + "bank_accounts", + { + id: uuid().defaultRandom().primaryKey().notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) + .defaultNow() + .notNull(), + createdBy: uuid("created_by").notNull(), + teamId: uuid("team_id").notNull(), + name: text(), + currency: text(), + bankConnectionId: uuid("bank_connection_id"), + enabled: boolean().default(true).notNull(), + accountId: text("account_id").notNull(), + balance: numericCasted({ precision: 10, scale: 2 }).default(0), + manual: boolean().default(false), + type: accountTypeEnum(), + baseCurrency: text("base_currency"), + baseBalance: numericCasted({ precision: 10, scale: 2 }), + errorDetails: text("error_details"), + errorRetries: smallint("error_retries"), + accountReference: text("account_reference"), + }, + (table) => [ + index("bank_accounts_bank_connection_id_idx").using( + "btree", + table.bankConnectionId.asc().nullsLast().op("uuid_ops"), + ), + index("bank_accounts_created_by_idx").using( + "btree", + table.createdBy.asc().nullsLast().op("uuid_ops"), + ), + index("bank_accounts_team_id_idx").using( + "btree", + table.teamId.asc().nullsLast().op("uuid_ops"), + ), + foreignKey({ + columns: [table.bankConnectionId], + foreignColumns: [bankConnections.id], + name: "bank_accounts_bank_connection_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.createdBy], + foreignColumns: [users.id], + name: "bank_accounts_created_by_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.teamId], + foreignColumns: [teams.id], + name: "public_bank_accounts_team_id_fkey", + }).onDelete("cascade"), + pgPolicy("Bank Accounts can be created by a member of the team", { + as: "permissive", + for: "insert", + to: ["public"], + withCheck: sql`(team_id IN ( SELECT private.get_teams_for_authenticated_user() AS get_teams_for_authenticated_user))`, + }), + pgPolicy("Bank Accounts can be deleted by a member of the team", { + as: "permissive", + for: "delete", + to: ["public"], + }), + pgPolicy("Bank Accounts can be selected by a member of the team", { + as: "permissive", + for: "select", + to: ["public"], + }), + pgPolicy("Bank Accounts can be updated by a member of the team", { + as: "permissive", + for: "update", + to: ["public"], + }), + ], +); + +export const invoices = pgTable( + "invoices", + { + id: uuid().defaultRandom().primaryKey().notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) + .defaultNow() + .notNull(), + updatedAt: timestamp("updated_at", { + withTimezone: true, + mode: "string", + }).defaultNow(), + dueDate: timestamp("due_date", { withTimezone: true, mode: "string" }), + invoiceNumber: text("invoice_number"), + customerId: uuid("customer_id"), + amount: numericCasted({ precision: 10, scale: 2 }), + currency: text(), + lineItems: jsonb("line_items"), + paymentDetails: jsonb("payment_details"), + customerDetails: jsonb("customer_details"), + companyDatails: jsonb("company_datails"), + note: text(), + internalNote: text("internal_note"), + teamId: uuid("team_id").notNull(), + paidAt: timestamp("paid_at", { withTimezone: true, mode: "string" }), + fts: tsvector("fts") + .notNull() + .generatedAlwaysAs( + (): SQL => sql` + to_tsvector( + 'english', + ( + (COALESCE((amount)::text, ''::text) || ' '::text) || COALESCE(invoice_number, ''::text) + ) + ) + `, + ), + vat: numericCasted({ precision: 10, scale: 2 }), + tax: numericCasted({ precision: 10, scale: 2 }), + url: text(), + filePath: text("file_path").array(), + status: invoiceStatusEnum().default("draft").notNull(), + viewedAt: timestamp("viewed_at", { withTimezone: true, mode: "string" }), + fromDetails: jsonb("from_details"), + issueDate: timestamp("issue_date", { withTimezone: true, mode: "string" }), + template: jsonb(), + noteDetails: jsonb("note_details"), + customerName: text("customer_name"), + token: text().default("").notNull(), + sentTo: text("sent_to"), + reminderSentAt: timestamp("reminder_sent_at", { + withTimezone: true, + mode: "string", + }), + discount: numericCasted({ precision: 10, scale: 2 }), + fileSize: bigint("file_size", { mode: "number" }), + userId: uuid("user_id"), + subtotal: numericCasted({ precision: 10, scale: 2 }), + topBlock: jsonb("top_block"), + bottomBlock: jsonb("bottom_block"), + sentAt: timestamp("sent_at", { withTimezone: true, mode: "string" }), + scheduledAt: timestamp("scheduled_at", { + withTimezone: true, + mode: "string", + }), + scheduledJobId: text("scheduled_job_id"), + }, + (table) => [ + index("invoices_created_at_idx").using( + "btree", + table.createdAt.asc().nullsLast().op("timestamptz_ops"), + ), + index("invoices_fts").using( + "gin", + table.fts.asc().nullsLast().op("tsvector_ops"), + ), + index("invoices_team_id_idx").using( + "btree", + table.teamId.asc().nullsLast().op("uuid_ops"), + ), + foreignKey({ + columns: [table.userId], + foreignColumns: [users.id], + name: "invoices_created_by_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.customerId], + foreignColumns: [customers.id], + name: "invoices_customer_id_fkey", + }).onDelete("set null"), + foreignKey({ + columns: [table.teamId], + foreignColumns: [teams.id], + name: "invoices_team_id_fkey", + }).onDelete("cascade"), + unique("invoices_scheduled_job_id_key").on(table.scheduledJobId), + pgPolicy("Invoices can be handled by a member of the team", { + as: "permissive", + for: "all", + to: ["public"], + using: sql`(team_id IN ( SELECT private.get_teams_for_authenticated_user() AS get_teams_for_authenticated_user))`, + }), + ], +); + +export const customers = pgTable( + "customers", + { + id: uuid().defaultRandom().primaryKey().notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) + .defaultNow() + .notNull(), + name: text().notNull(), + email: text().notNull(), + billingEmail: text(), + country: text(), + addressLine1: text("address_line_1"), + addressLine2: text("address_line_2"), + city: text(), + state: text(), + zip: text(), + note: text(), + teamId: uuid("team_id").defaultRandom().notNull(), + website: text(), + phone: text(), + vatNumber: text("vat_number"), + countryCode: text("country_code"), + token: text().default("").notNull(), + contact: text(), + fts: tsvector("fts") + .notNull() + .generatedAlwaysAs( + (): SQL => sql` + to_tsvector( + 'english'::regconfig, + COALESCE(name, ''::text) || ' ' || + COALESCE(contact, ''::text) || ' ' || + COALESCE(phone, ''::text) || ' ' || + COALESCE(email, ''::text) || ' ' || + COALESCE(address_line_1, ''::text) || ' ' || + COALESCE(address_line_2, ''::text) || ' ' || + COALESCE(city, ''::text) || ' ' || + COALESCE(state, ''::text) || ' ' || + COALESCE(zip, ''::text) || ' ' || + COALESCE(country, ''::text) + ) + `, + ), + }, + (table) => [ + index("customers_fts").using( + "gin", + table.fts.asc().nullsLast().op("tsvector_ops"), + ), + foreignKey({ + columns: [table.teamId], + foreignColumns: [teams.id], + name: "customers_team_id_fkey", + }).onDelete("cascade"), + pgPolicy("Customers can be handled by members of the team", { + as: "permissive", + for: "all", + to: ["public"], + using: sql`(team_id IN ( SELECT private.get_teams_for_authenticated_user() AS get_teams_for_authenticated_user))`, + }), + ], +); + +export const exchangeRates = pgTable( + "exchange_rates", + { + id: uuid().defaultRandom().primaryKey().notNull(), + base: text(), + rate: numericCasted({ precision: 10, scale: 2 }), + target: text(), + updatedAt: timestamp("updated_at", { withTimezone: true, mode: "string" }), + }, + (table) => [ + index("exchange_rates_base_target_idx").using( + "btree", + table.base.asc().nullsLast().op("text_ops"), + table.target.asc().nullsLast().op("text_ops"), + ), + unique("unique_rate").on(table.base, table.target), + pgPolicy("Enable read access for authenticated users", { + as: "permissive", + for: "select", + to: ["public"], + using: sql`true`, + }), + ], +); + +export const tags = pgTable( + "tags", + { + id: uuid().defaultRandom().primaryKey().notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) + .defaultNow() + .notNull(), + teamId: uuid("team_id").notNull(), + name: text().notNull(), + }, + (table) => [ + index("tags_team_id_idx").using( + "btree", + table.teamId.asc().nullsLast().op("uuid_ops"), + ), + foreignKey({ + columns: [table.teamId], + foreignColumns: [teams.id], + name: "tags_team_id_fkey", + }).onDelete("cascade"), + unique("unique_tag_name").on(table.teamId, table.name), + pgPolicy("Tags can be handled by a member of the team", { + as: "permissive", + for: "all", + to: ["public"], + using: sql`(team_id IN ( SELECT private.get_teams_for_authenticated_user() AS get_teams_for_authenticated_user))`, + }), + ], +); + +export const trackerReports = pgTable( + "tracker_reports", + { + id: uuid().defaultRandom().primaryKey().notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) + .defaultNow() + .notNull(), + linkId: text("link_id"), + shortLink: text("short_link"), + teamId: uuid("team_id").defaultRandom(), + projectId: uuid("project_id").defaultRandom(), + createdBy: uuid("created_by"), + }, + (table) => [ + index("tracker_reports_team_id_idx").using( + "btree", + table.teamId.asc().nullsLast().op("uuid_ops"), + ), + foreignKey({ + columns: [table.createdBy], + foreignColumns: [users.id], + name: "public_tracker_reports_created_by_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.projectId], + foreignColumns: [trackerProjects.id], + name: "public_tracker_reports_project_id_fkey", + }) + .onUpdate("cascade") + .onDelete("cascade"), + foreignKey({ + columns: [table.teamId], + foreignColumns: [teams.id], + name: "tracker_reports_team_id_fkey", + }) + .onUpdate("cascade") + .onDelete("cascade"), + pgPolicy("Reports can be handled by a member of the team", { + as: "permissive", + for: "all", + to: ["public"], + using: sql`(team_id IN ( SELECT private.get_teams_for_authenticated_user() AS get_teams_for_authenticated_user))`, + }), + ], +); + +export const invoiceComments = pgTable("invoice_comments", { + id: uuid().defaultRandom().primaryKey().notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) + .defaultNow() + .notNull(), +}); + +export const trackerProjectTags = pgTable( + "tracker_project_tags", + { + id: uuid().defaultRandom().primaryKey().notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) + .defaultNow() + .notNull(), + trackerProjectId: uuid("tracker_project_id").notNull(), + tagId: uuid("tag_id").notNull(), + teamId: uuid("team_id").notNull(), + }, + (table) => [ + index("tracker_project_tags_team_id_idx").using( + "btree", + table.teamId.asc().nullsLast().op("uuid_ops"), + ), + index("tracker_project_tags_tracker_project_id_tag_id_team_id_idx").using( + "btree", + table.trackerProjectId.asc().nullsLast().op("uuid_ops"), + table.tagId.asc().nullsLast().op("uuid_ops"), + table.teamId.asc().nullsLast().op("uuid_ops"), + ), + foreignKey({ + columns: [table.tagId], + foreignColumns: [tags.id], + name: "project_tags_tag_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.trackerProjectId], + foreignColumns: [trackerProjects.id], + name: "project_tags_tracker_project_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.teamId], + foreignColumns: [teams.id], + name: "tracker_project_tags_team_id_fkey", + }).onDelete("cascade"), + unique("unique_project_tag").on(table.trackerProjectId, table.tagId), + pgPolicy("Tags can be handled by a member of the team", { + as: "permissive", + for: "all", + to: ["public"], + using: sql`(team_id IN ( SELECT private.get_teams_for_authenticated_user() AS get_teams_for_authenticated_user))`, + }), + ], +); + +export const reports = pgTable( + "reports", + { + id: uuid().defaultRandom().primaryKey().notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) + .defaultNow() + .notNull(), + linkId: text("link_id"), + teamId: uuid("team_id"), + shortLink: text("short_link"), + from: timestamp({ withTimezone: true, mode: "string" }), + to: timestamp({ withTimezone: true, mode: "string" }), + type: reportTypesEnum(), + expireAt: timestamp("expire_at", { withTimezone: true, mode: "string" }), + currency: text(), + createdBy: uuid("created_by"), + }, + (table) => [ + index("reports_team_id_idx").using( + "btree", + table.teamId.asc().nullsLast().op("uuid_ops"), + ), + foreignKey({ + columns: [table.createdBy], + foreignColumns: [users.id], + name: "public_reports_created_by_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.teamId], + foreignColumns: [teams.id], + name: "reports_team_id_fkey", + }).onDelete("cascade"), + pgPolicy("Reports can be created by a member of the team", { + as: "permissive", + for: "insert", + to: ["public"], + withCheck: sql`(team_id IN ( SELECT private.get_teams_for_authenticated_user() AS get_teams_for_authenticated_user))`, + }), + pgPolicy("Reports can be deleted by a member of the team", { + as: "permissive", + for: "delete", + to: ["public"], + }), + pgPolicy("Reports can be selected by a member of the team", { + as: "permissive", + for: "select", + to: ["public"], + }), + pgPolicy("Reports can be updated by member of team", { + as: "permissive", + for: "update", + to: ["public"], + }), + ], +); + +export const bankConnections = pgTable( + "bank_connections", + { + id: uuid().defaultRandom().primaryKey().notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) + .defaultNow() + .notNull(), + institutionId: text("institution_id").notNull(), + expiresAt: timestamp("expires_at", { withTimezone: true, mode: "string" }), + teamId: uuid("team_id").notNull(), + name: text().notNull(), + logoUrl: text("logo_url"), + accessToken: text("access_token"), + enrollmentId: text("enrollment_id"), + provider: bankProvidersEnum().notNull(), + lastAccessed: timestamp("last_accessed", { + withTimezone: true, + mode: "string", + }), + referenceId: text("reference_id"), + status: connectionStatusEnum().default("connected"), + errorDetails: text("error_details"), + errorRetries: smallint("error_retries").default(sql`'0'`), + }, + (table) => [ + index("bank_connections_team_id_idx").using( + "btree", + table.teamId.asc().nullsLast().op("uuid_ops"), + ), + foreignKey({ + columns: [table.teamId], + foreignColumns: [teams.id], + name: "bank_connections_team_id_fkey", + }).onDelete("cascade"), + unique("unique_bank_connections").on(table.institutionId, table.teamId), + pgPolicy("Bank Connections can be created by a member of the team", { + as: "permissive", + for: "insert", + to: ["public"], + withCheck: sql`(team_id IN ( SELECT private.get_teams_for_authenticated_user() AS get_teams_for_authenticated_user))`, + }), + pgPolicy("Bank Connections can be deleted by a member of the team", { + as: "permissive", + for: "delete", + to: ["public"], + }), + pgPolicy("Bank Connections can be selected by a member of the team", { + as: "permissive", + for: "select", + to: ["public"], + }), + pgPolicy("Bank Connections can be updated by a member of the team", { + as: "permissive", + for: "update", + to: ["public"], + }), + ], +); + +export const userInvites = pgTable( + "user_invites", + { + id: uuid().defaultRandom().primaryKey().notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) + .defaultNow() + .notNull(), + teamId: uuid("team_id"), + email: text(), + role: teamRolesEnum(), + code: text().default("nanoid(24)"), + invitedBy: uuid("invited_by"), + }, + (table) => [ + index("user_invites_team_id_idx").using( + "btree", + table.teamId.asc().nullsLast().op("uuid_ops"), + ), + foreignKey({ + columns: [table.teamId], + foreignColumns: [teams.id], + name: "public_user_invites_team_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.invitedBy], + foreignColumns: [users.id], + name: "user_invites_invited_by_fkey", + }).onDelete("cascade"), + unique("unique_team_invite").on(table.teamId, table.email), + unique("user_invites_code_key").on(table.code), + pgPolicy("Enable select for users based on email", { + as: "permissive", + for: "select", + to: ["public"], + using: sql`((auth.jwt() ->> 'email'::text) = email)`, + }), + pgPolicy("User Invites can be created by a member of the team", { + as: "permissive", + for: "insert", + to: ["public"], + }), + pgPolicy("User Invites can be deleted by a member of the team", { + as: "permissive", + for: "delete", + to: ["public"], + }), + pgPolicy("User Invites can be deleted by invited email", { + as: "permissive", + for: "delete", + to: ["public"], + }), + pgPolicy("User Invites can be selected by a member of the team", { + as: "permissive", + for: "select", + to: ["public"], + }), + pgPolicy("User Invites can be updated by a member of the team", { + as: "permissive", + for: "update", + to: ["public"], + }), + ], +); + +export const documentTags = pgTable( + "document_tags", + { + id: uuid().defaultRandom().primaryKey().notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) + .defaultNow() + .notNull(), + name: text().notNull(), + slug: text().notNull(), + teamId: uuid("team_id").notNull(), + }, + (table) => [ + foreignKey({ + columns: [table.teamId], + foreignColumns: [teams.id], + name: "document_tags_team_id_fkey", + }).onDelete("cascade"), + unique("unique_slug_per_team").on(table.slug, table.teamId), + pgPolicy("Tags can be handled by a member of the team", { + as: "permissive", + for: "all", + to: ["public"], + using: sql`(team_id IN ( SELECT private.get_teams_for_authenticated_user() AS get_teams_for_authenticated_user))`, + }), + ], +); + +export const transactionTags = pgTable( + "transaction_tags", + { + id: uuid().defaultRandom().primaryKey().notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) + .defaultNow() + .notNull(), + teamId: uuid("team_id").notNull(), + tagId: uuid("tag_id").notNull(), + transactionId: uuid("transaction_id").notNull(), + }, + (table) => [ + index("transaction_tags_tag_id_idx").using( + "btree", + table.tagId.asc().nullsLast().op("uuid_ops"), + ), + index("transaction_tags_team_id_idx").using( + "btree", + table.teamId.asc().nullsLast().op("uuid_ops"), + ), + index("transaction_tags_transaction_id_tag_id_team_id_idx").using( + "btree", + table.transactionId.asc().nullsLast().op("uuid_ops"), + table.tagId.asc().nullsLast().op("uuid_ops"), + table.teamId.asc().nullsLast().op("uuid_ops"), + ), + foreignKey({ + columns: [table.tagId], + foreignColumns: [tags.id], + name: "transaction_tags_tag_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.teamId], + foreignColumns: [teams.id], + name: "transaction_tags_team_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.transactionId], + foreignColumns: [transactions.id], + name: "transaction_tags_transaction_id_fkey", + }).onDelete("cascade"), + unique("unique_tag").on(table.tagId, table.transactionId), + pgPolicy("Transaction Tags can be handled by a member of the team", { + as: "permissive", + for: "all", + to: ["public"], + using: sql`(team_id IN ( SELECT private.get_teams_for_authenticated_user() AS get_teams_for_authenticated_user))`, + }), + ], +); + +export const transactionAttachments = pgTable( + "transaction_attachments", + { + id: uuid().defaultRandom().primaryKey().notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) + .defaultNow() + .notNull(), + type: text(), + transactionId: uuid("transaction_id"), + teamId: uuid("team_id"), + // You can use { mode: "bigint" } if numbers are exceeding js number limitations + size: bigint({ mode: "number" }), + name: text(), + path: text().array(), + }, + (table) => [ + index("transaction_attachments_team_id_idx").using( + "btree", + table.teamId.asc().nullsLast().op("uuid_ops"), + ), + index("transaction_attachments_transaction_id_idx").using( + "btree", + table.transactionId.asc().nullsLast().op("uuid_ops"), + ), + foreignKey({ + columns: [table.teamId], + foreignColumns: [teams.id], + name: "public_transaction_attachments_team_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.transactionId], + foreignColumns: [transactions.id], + name: "public_transaction_attachments_transaction_id_fkey", + }).onDelete("set null"), + pgPolicy("Transaction Attachments can be created by a member of the team", { + as: "permissive", + for: "insert", + to: ["public"], + withCheck: sql`(team_id IN ( SELECT private.get_teams_for_authenticated_user() AS get_teams_for_authenticated_user))`, + }), + pgPolicy("Transaction Attachments can be deleted by a member of the team", { + as: "permissive", + for: "delete", + to: ["public"], + }), + pgPolicy( + "Transaction Attachments can be selected by a member of the team", + { as: "permissive", for: "select", to: ["public"] }, + ), + pgPolicy("Transaction Attachments can be updated by a member of the team", { + as: "permissive", + for: "update", + to: ["public"], + }), + ], +); + +export const teams = pgTable( + "teams", + { + id: uuid().defaultRandom().primaryKey().notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) + .defaultNow() + .notNull(), + name: text(), + logoUrl: text("logo_url"), + inboxId: text("inbox_id").default("generate_inbox(10)"), + email: text(), + inboxEmail: text("inbox_email"), + inboxForwarding: boolean("inbox_forwarding").default(true), + baseCurrency: text("base_currency"), + countryCode: text("country_code"), + documentClassification: boolean("document_classification").default(false), + flags: text().array(), + canceledAt: timestamp("canceled_at", { + withTimezone: true, + mode: "string", + }), + plan: plansEnum().default("trial").notNull(), + // subscriptionStatus: subscriptionStatusEnum("subscription_status"), + exportSettings: jsonb("export_settings"), + }, + (table) => [ + unique("teams_inbox_id_key").on(table.inboxId), + pgPolicy("Enable insert for authenticated users only", { + as: "permissive", + for: "insert", + to: ["authenticated"], + withCheck: sql`true`, + }), + pgPolicy("Invited users can select team if they are invited.", { + as: "permissive", + for: "select", + to: ["public"], + }), + pgPolicy("Teams can be deleted by a member of the team", { + as: "permissive", + for: "delete", + to: ["public"], + }), + pgPolicy("Teams can be selected by a member of the team", { + as: "permissive", + for: "select", + to: ["public"], + }), + pgPolicy("Teams can be updated by a member of the team", { + as: "permissive", + for: "update", + to: ["public"], + }), + ], +); + +export const documents = pgTable( + "documents", + { + id: uuid().defaultRandom().primaryKey().notNull(), + name: text(), + createdAt: timestamp("created_at", { + withTimezone: true, + mode: "string", + }).defaultNow(), + metadata: jsonb(), + pathTokens: text("path_tokens").array(), + teamId: uuid("team_id"), + parentId: text("parent_id"), + objectId: uuid("object_id"), + ownerId: uuid("owner_id"), + tag: text(), + title: text(), + body: text(), + fts: tsvector("fts") + .notNull() + .generatedAlwaysAs( + (): SQL => + sql`to_tsvector('english'::regconfig, ((title || ' '::text) || body))`, + ), + summary: text(), + content: text(), + date: date(), + language: text(), + processingStatus: + documentProcessingStatusEnum("processing_status").default("pending"), + ftsSimple: tsvector("fts_simple"), + ftsEnglish: tsvector("fts_english"), + ftsLanguage: tsvector("fts_language"), + }, + (table) => [ + index("documents_name_idx").using( + "btree", + table.name.asc().nullsLast().op("text_ops"), + ), + index("documents_team_id_idx").using( + "btree", + table.teamId.asc().nullsLast().op("uuid_ops"), + ), + index("documents_team_id_parent_id_idx").using( + "btree", + table.teamId.asc().nullsLast().op("text_ops"), + table.parentId.asc().nullsLast().op("text_ops"), + ), + index("idx_documents_fts_english").using( + "gin", + table.ftsEnglish.asc().nullsLast().op("tsvector_ops"), + ), + index("idx_documents_fts_language").using( + "gin", + table.ftsLanguage.asc().nullsLast().op("tsvector_ops"), + ), + index("idx_documents_fts_simple").using( + "gin", + table.ftsSimple.asc().nullsLast().op("tsvector_ops"), + ), + index("idx_gin_documents_title").using( + "gin", + table.title.asc().nullsLast().op("gin_trgm_ops"), + ), + index("idx_gin_documents_name").using( + "gin", + table.name.asc().nullsLast().op("gin_trgm_ops"), + ), + foreignKey({ + columns: [table.ownerId], + foreignColumns: [users.id], + name: "documents_created_by_fkey", + }).onDelete("set null"), + foreignKey({ + columns: [table.teamId], + foreignColumns: [teams.id], + name: "storage_team_id_fkey", + }).onDelete("cascade"), + pgPolicy("Documents can be deleted by a member of the team", { + as: "permissive", + for: "all", + to: ["public"], + using: sql`(team_id IN ( SELECT private.get_teams_for_authenticated_user() AS get_teams_for_authenticated_user))`, + }), + pgPolicy("Documents can be selected by a member of the team", { + as: "permissive", + for: "all", + to: ["public"], + }), + pgPolicy("Documents can be updated by a member of the team", { + as: "permissive", + for: "update", + to: ["public"], + }), + pgPolicy("Enable insert for authenticated users only", { + as: "permissive", + for: "insert", + to: ["authenticated"], + }), + ], +); + +export const apps = pgTable( + "apps", + { + id: uuid().defaultRandom().primaryKey().notNull(), + teamId: uuid("team_id").defaultRandom(), + config: jsonb(), + createdAt: timestamp("created_at", { + withTimezone: true, + mode: "string", + }).defaultNow(), + appId: text("app_id").notNull(), + createdBy: uuid("created_by").defaultRandom(), + settings: jsonb(), + }, + (table) => [ + foreignKey({ + columns: [table.createdBy], + foreignColumns: [users.id], + name: "apps_created_by_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.teamId], + foreignColumns: [teams.id], + name: "integrations_team_id_fkey", + }).onDelete("cascade"), + unique("unique_app_id_team_id").on(table.teamId, table.appId), + pgPolicy("Apps can be deleted by a member of the team", { + as: "permissive", + for: "delete", + to: ["public"], + using: sql`(team_id IN ( SELECT private.get_teams_for_authenticated_user() AS get_teams_for_authenticated_user))`, + }), + pgPolicy("Apps can be inserted by a member of the team", { + as: "permissive", + for: "insert", + to: ["public"], + }), + pgPolicy("Apps can be selected by a member of the team", { + as: "permissive", + for: "select", + to: ["public"], + }), + pgPolicy("Apps can be updated by a member of the team", { + as: "permissive", + for: "update", + to: ["public"], + }), + ], +); + +export const invoiceTemplates = pgTable( + "invoice_templates", + { + id: uuid().defaultRandom().primaryKey().notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) + .defaultNow() + .notNull(), + teamId: uuid("team_id").notNull(), + customerLabel: text("customer_label"), + fromLabel: text("from_label"), + invoiceNoLabel: text("invoice_no_label"), + issueDateLabel: text("issue_date_label"), + dueDateLabel: text("due_date_label"), + descriptionLabel: text("description_label"), + priceLabel: text("price_label"), + quantityLabel: text("quantity_label"), + totalLabel: text("total_label"), + vatLabel: text("vat_label"), + taxLabel: text("tax_label"), + paymentLabel: text("payment_label"), + noteLabel: text("note_label"), + logoUrl: text("logo_url"), + currency: text(), + paymentDetails: jsonb("payment_details"), + fromDetails: jsonb("from_details"), + noteDetails: jsonb("note_details"), + size: invoiceSizeEnum().default("a4"), + dateFormat: text("date_format"), + includeVat: boolean("include_vat"), + includeTax: boolean("include_tax"), + taxRate: numericCasted("tax_rate", { precision: 10, scale: 2 }), + deliveryType: invoiceDeliveryTypeEnum("delivery_type") + .default("create") + .notNull(), + discountLabel: text("discount_label"), + includeDiscount: boolean("include_discount"), + includeDecimals: boolean("include_decimals"), + includeQr: boolean("include_qr"), + totalSummaryLabel: text("total_summary_label"), + title: text(), + vatRate: numericCasted("vat_rate", { precision: 10, scale: 2 }), + includeUnits: boolean("include_units"), + subtotalLabel: text("subtotal_label"), + includePdf: boolean("include_pdf"), + sendCopy: boolean("send_copy"), + }, + (table) => [ + foreignKey({ + columns: [table.teamId], + foreignColumns: [teams.id], + name: "invoice_settings_team_id_fkey", + }).onDelete("cascade"), + unique("invoice_templates_team_id_key").on(table.teamId), + pgPolicy("Invoice templates can be handled by a member of the team", { + as: "permissive", + for: "all", + to: ["public"], + using: sql`(team_id IN ( SELECT private.get_teams_for_authenticated_user() AS get_teams_for_authenticated_user))`, + }), + ], +); + +export const invoiceProducts = pgTable( + "invoice_products", + { + id: uuid().defaultRandom().primaryKey().notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) + .defaultNow() + .notNull(), + updatedAt: timestamp("updated_at", { + withTimezone: true, + mode: "string", + }).defaultNow(), + teamId: uuid("team_id").notNull(), + createdBy: uuid("created_by"), + name: text().notNull(), + description: text(), + price: numericCasted({ precision: 10, scale: 2 }), + currency: text(), + unit: text(), + isActive: boolean().default(true).notNull(), + usageCount: integer("usage_count").default(0).notNull(), + lastUsedAt: timestamp("last_used_at", { + withTimezone: true, + mode: "string", + }), + // Full-text search for product names and descriptions + fts: tsvector("fts") + .notNull() + .generatedAlwaysAs( + (): SQL => sql` + to_tsvector( + 'english', + ( + (COALESCE(name, ''::text) || ' '::text) || COALESCE(description, ''::text) + ) + ) + `, + ), + }, + (table) => [ + foreignKey({ + columns: [table.teamId], + foreignColumns: [teams.id], + name: "invoice_products_team_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.createdBy], + foreignColumns: [users.id], + name: "invoice_products_created_by_fkey", + }).onDelete("set null"), + index("invoice_products_team_id_idx").on(table.teamId), + index("invoice_products_created_by_idx").on(table.createdBy), + index("invoice_products_fts_idx").using("gin", table.fts), + index("invoice_products_name_idx").on(table.name), + index("invoice_products_usage_count_idx").on(table.usageCount), + index("invoice_products_last_used_at_idx").on(table.lastUsedAt), + // Composite index for team + active status for fast filtering + index("invoice_products_team_active_idx").on(table.teamId, table.isActive), + // Unique constraint for upsert operations (team + name + currency + price combination) + unique("invoice_products_team_name_currency_price_unique").on( + table.teamId, + table.name, + table.currency, + table.price, + ), + pgPolicy("Enable read access for team members", { + as: "permissive", + for: "select", + to: ["public"], + using: sql`team_id = (select auth.jwt() ->> 'team_id')::uuid`, + }), + pgPolicy("Enable insert access for team members", { + as: "permissive", + for: "insert", + to: ["public"], + withCheck: sql`team_id = (select auth.jwt() ->> 'team_id')::uuid`, + }), + pgPolicy("Enable update access for team members", { + as: "permissive", + for: "update", + to: ["public"], + using: sql`team_id = (select auth.jwt() ->> 'team_id')::uuid`, + }), + pgPolicy("Enable delete access for team members", { + as: "permissive", + for: "delete", + to: ["public"], + using: sql`team_id = (select auth.jwt() ->> 'team_id')::uuid`, + }), + ], +); + +export const transactionEnrichments = pgTable( + "transaction_enrichments", + { + id: uuid().defaultRandom().primaryKey().notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) + .defaultNow() + .notNull(), + name: text(), + teamId: uuid("team_id"), + categorySlug: text("category_slug"), + system: boolean().default(false), + }, + (table) => [ + index("transaction_enrichments_category_slug_team_id_idx").using( + "btree", + table.categorySlug.asc().nullsLast().op("text_ops"), + table.teamId.asc().nullsLast().op("uuid_ops"), + ), + foreignKey({ + columns: [table.teamId, table.categorySlug], + foreignColumns: [ + transactionCategories.teamId, + transactionCategories.slug, + ], + name: "transaction_enrichments_category_slug_team_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.teamId], + foreignColumns: [teams.id], + name: "transaction_enrichments_team_id_fkey", + }).onDelete("cascade"), + unique("unique_team_name").on(table.name, table.teamId), + pgPolicy("Enable insert for authenticated users only", { + as: "permissive", + for: "insert", + to: ["authenticated"], + withCheck: sql`true`, + }), + pgPolicy("Enable update for authenticated users only", { + as: "permissive", + for: "update", + to: ["authenticated"], + }), + ], +); + +export const users = pgTable( + "users", + { + id: uuid().primaryKey().notNull(), + fullName: text("full_name"), + avatarUrl: text("avatar_url"), + email: text(), + teamId: uuid("team_id"), + createdAt: timestamp("created_at", { + withTimezone: true, + mode: "string", + }).defaultNow(), + locale: text().default("en"), + weekStartsOnMonday: boolean("week_starts_on_monday").default(false), + timezone: text(), + timezoneAutoSync: boolean("timezone_auto_sync").default(true), + timeFormat: numericCasted("time_format").default(24), + dateFormat: text("date_format"), + }, + (table) => [ + index("users_team_id_idx").using( + "btree", + table.teamId.asc().nullsLast().op("uuid_ops"), + ), + foreignKey({ + columns: [table.id], + foreignColumns: [table.id], + name: "users_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.teamId], + foreignColumns: [teams.id], + name: "users_team_id_fkey", + }).onDelete("set null"), + pgPolicy("Users can insert their own profile.", { + as: "permissive", + for: "insert", + to: ["public"], + withCheck: sql`(auth.uid() = id)`, + }), + pgPolicy("Users can select their own profile.", { + as: "permissive", + for: "select", + to: ["public"], + }), + pgPolicy("Users can select users if they are in the same team", { + as: "permissive", + for: "select", + to: ["authenticated"], + }), + pgPolicy("Users can update own profile.", { + as: "permissive", + for: "update", + to: ["public"], + }), + ], +); + +export const trackerProjects = pgTable( + "tracker_projects", + { + id: uuid().defaultRandom().primaryKey().notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) + .defaultNow() + .notNull(), + teamId: uuid("team_id"), + rate: numericCasted({ precision: 10, scale: 2 }), + currency: text(), + status: trackerStatusEnum().default("in_progress").notNull(), + description: text(), + name: text().notNull(), + billable: boolean().default(false), + // You can use { mode: "bigint" } if numbers are exceeding js number limitations + estimate: bigint({ mode: "number" }), + customerId: uuid("customer_id"), + fts: tsvector("fts") + .notNull() + .generatedAlwaysAs( + (): SQL => sql` + to_tsvector( + 'english'::regconfig, + ( + (COALESCE(name, ''::text) || ' '::text) || COALESCE(description, ''::text) + ) + ) + `, + ), + }, + (table) => [ + index("tracker_projects_fts").using( + "gin", + table.fts.asc().nullsLast().op("tsvector_ops"), + ), + index("tracker_projects_team_id_idx").using( + "btree", + table.teamId.asc().nullsLast().op("uuid_ops"), + ), + foreignKey({ + columns: [table.customerId], + foreignColumns: [customers.id], + name: "tracker_projects_customer_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.teamId], + foreignColumns: [teams.id], + name: "tracker_projects_team_id_fkey", + }).onDelete("cascade"), + pgPolicy("Projects can be created by a member of the team", { + as: "permissive", + for: "insert", + to: ["authenticated"], + withCheck: sql`(team_id IN ( SELECT private.get_teams_for_authenticated_user() AS get_teams_for_authenticated_user))`, + }), + pgPolicy("Projects can be deleted by a member of the team", { + as: "permissive", + for: "delete", + to: ["authenticated"], + }), + pgPolicy("Projects can be selected by a member of the team", { + as: "permissive", + for: "select", + to: ["authenticated"], + }), + pgPolicy("Projects can be updated by a member of the team", { + as: "permissive", + for: "update", + to: ["authenticated"], + }), + ], +); + +export const inbox = pgTable( + "inbox", + { + id: uuid().defaultRandom().primaryKey().notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) + .defaultNow() + .notNull(), + teamId: uuid("team_id"), + filePath: text("file_path").array(), + fileName: text("file_name"), + transactionId: uuid("transaction_id"), + amount: numericCasted("amount", { precision: 10, scale: 2 }), + currency: text(), + contentType: text("content_type"), + // You can use { mode: "bigint" } if numbers are exceeding js number limitations + size: bigint({ mode: "number" }), + attachmentId: uuid("attachment_id"), + date: date(), + forwardedTo: text("forwarded_to"), + referenceId: text("reference_id"), + meta: json(), + status: inboxStatusEnum().default("new"), + website: text(), + displayName: text("display_name"), + fts: tsvector("fts") + .notNull() + .generatedAlwaysAs( + (): SQL => + sql`generate_inbox_fts(display_name, extract_product_names((meta -> 'products'::text)))`, + ), + type: inboxTypeEnum(), + description: text(), + baseAmount: numericCasted("base_amount", { precision: 10, scale: 2 }), + baseCurrency: text("base_currency"), + taxAmount: numericCasted("tax_amount", { precision: 10, scale: 2 }), + taxRate: numericCasted("tax_rate", { precision: 10, scale: 2 }), + taxType: text("tax_type"), + inboxAccountId: uuid("inbox_account_id"), + }, + (table) => [ + index("inbox_attachment_id_idx").using( + "btree", + table.attachmentId.asc().nullsLast().op("uuid_ops"), + ), + index("inbox_created_at_idx").using( + "btree", + table.createdAt.asc().nullsLast().op("timestamptz_ops"), + ), + index("inbox_team_id_idx").using( + "btree", + table.teamId.asc().nullsLast().op("uuid_ops"), + ), + index("inbox_transaction_id_idx").using( + "btree", + table.transactionId.asc().nullsLast().op("uuid_ops"), + ), + index("inbox_inbox_account_id_idx").using( + "btree", + table.inboxAccountId.asc().nullsLast().op("uuid_ops"), + ), + foreignKey({ + columns: [table.attachmentId], + foreignColumns: [transactionAttachments.id], + name: "inbox_attachment_id_fkey", + }).onDelete("set null"), + foreignKey({ + columns: [table.teamId], + foreignColumns: [teams.id], + name: "public_inbox_team_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.transactionId], + foreignColumns: [transactions.id], + name: "public_inbox_transaction_id_fkey", + }).onDelete("set null"), + foreignKey({ + columns: [table.inboxAccountId], + foreignColumns: [inboxAccounts.id], + name: "inbox_inbox_account_id_fkey", + }).onDelete("set null"), + unique("inbox_reference_id_key").on(table.referenceId), + pgPolicy("Inbox can be deleted by a member of the team", { + as: "permissive", + for: "delete", + to: ["public"], + using: sql`(team_id IN ( SELECT private.get_teams_for_authenticated_user() AS get_teams_for_authenticated_user))`, + }), + pgPolicy("Inbox can be selected by a member of the team", { + as: "permissive", + for: "select", + to: ["public"], + }), + pgPolicy("Inbox can be updated by a member of the team", { + as: "permissive", + for: "update", + to: ["public"], + }), + ], +); + +export const transactionEmbeddings = pgTable( + "transaction_embeddings", + { + id: uuid().defaultRandom().primaryKey().notNull(), + transactionId: uuid("transaction_id").notNull(), + teamId: uuid("team_id").notNull(), + embedding: vector("embedding", { dimensions: 768 }), + sourceText: text("source_text").notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) + .defaultNow() + .notNull(), + model: text("model").notNull().default("gemini-embedding-001"), + }, + (table) => [ + index("transaction_embeddings_transaction_id_idx").using( + "btree", + table.transactionId.asc().nullsLast().op("uuid_ops"), + ), + index("transaction_embeddings_team_id_idx").using( + "btree", + table.teamId.asc().nullsLast().op("uuid_ops"), + ), + // Vector similarity index for fast cosine similarity searches + index("transaction_embeddings_vector_idx").using( + "hnsw", + table.embedding.op("vector_cosine_ops"), + ), + foreignKey({ + columns: [table.transactionId], + foreignColumns: [transactions.id], + name: "transaction_embeddings_transaction_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.teamId], + foreignColumns: [teams.id], + name: "transaction_embeddings_team_id_fkey", + }).onDelete("cascade"), + unique("transaction_embeddings_unique").on(table.transactionId), + ], +); + +export const inboxEmbeddings = pgTable( + "inbox_embeddings", + { + id: uuid().defaultRandom().primaryKey().notNull(), + inboxId: uuid("inbox_id").notNull(), + teamId: uuid("team_id").notNull(), + embedding: vector("embedding", { dimensions: 768 }), + sourceText: text("source_text").notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) + .defaultNow() + .notNull(), + model: text("model").notNull().default("gemini-embedding-001"), + }, + (table) => [ + index("inbox_embeddings_inbox_id_idx").using( + "btree", + table.inboxId.asc().nullsLast().op("uuid_ops"), + ), + index("inbox_embeddings_team_id_idx").using( + "btree", + table.teamId.asc().nullsLast().op("uuid_ops"), + ), + // Vector similarity index for fast cosine similarity searches + index("inbox_embeddings_vector_idx").using( + "hnsw", + table.embedding.op("vector_cosine_ops"), + ), + foreignKey({ + columns: [table.inboxId], + foreignColumns: [inbox.id], + name: "inbox_embeddings_inbox_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.teamId], + foreignColumns: [teams.id], + name: "inbox_embeddings_team_id_fkey", + }).onDelete("cascade"), + unique("inbox_embeddings_unique").on(table.inboxId), + ], +); + +export const transactionMatchSuggestions = pgTable( + "transaction_match_suggestions", + { + id: uuid().defaultRandom().primaryKey().notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) + .defaultNow() + .notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true, mode: "string" }) + .defaultNow() + .notNull(), + + // Core relationship + teamId: uuid("team_id").notNull(), + inboxId: uuid("inbox_id").notNull(), + transactionId: uuid("transaction_id").notNull(), + + // Match scores for transparency + confidenceScore: numericCasted("confidence_score", { + precision: 4, + scale: 3, + }).notNull(), + amountScore: numericCasted("amount_score", { precision: 4, scale: 3 }), + currencyScore: numericCasted("currency_score", { precision: 4, scale: 3 }), + dateScore: numericCasted("date_score", { precision: 4, scale: 3 }), + embeddingScore: numericCasted("embedding_score", { + precision: 4, + scale: 3, + }), + nameScore: numericCasted("name_score", { precision: 4, scale: 3 }), + + // Match context + matchType: text("match_type").notNull(), // 'auto_matched', 'high_confidence', 'suggested' + matchDetails: jsonb("match_details"), + + // User interaction tracking + status: text("status").default("pending").notNull(), // 'pending', 'confirmed', 'declined', 'expired', 'unmatched' + userActionAt: timestamp("user_action_at", { + withTimezone: true, + mode: "string", + }), + userId: uuid("user_id"), + }, + (table) => [ + index("transaction_match_suggestions_inbox_id_idx").using( + "btree", + table.inboxId.asc().nullsLast().op("uuid_ops"), + ), + index("transaction_match_suggestions_transaction_id_idx").using( + "btree", + table.transactionId.asc().nullsLast().op("uuid_ops"), + ), + index("transaction_match_suggestions_team_id_idx").using( + "btree", + table.teamId.asc().nullsLast().op("uuid_ops"), + ), + index("transaction_match_suggestions_status_idx").using( + "btree", + table.status.asc().nullsLast().op("text_ops"), + ), + index("transaction_match_suggestions_confidence_idx").using( + "btree", + table.confidenceScore.desc().nullsLast(), + ), + index("transaction_match_suggestions_lookup_idx").using( + "btree", + table.transactionId.asc().nullsLast().op("uuid_ops"), + table.teamId.asc().nullsLast().op("uuid_ops"), + table.status.asc().nullsLast().op("text_ops"), + ), + foreignKey({ + columns: [table.inboxId], + foreignColumns: [inbox.id], + name: "transaction_match_suggestions_inbox_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.transactionId], + foreignColumns: [transactions.id], + name: "transaction_match_suggestions_transaction_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.teamId], + foreignColumns: [teams.id], + name: "transaction_match_suggestions_team_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.userId], + foreignColumns: [users.id], + name: "transaction_match_suggestions_user_id_fkey", + }).onDelete("set null"), + unique("transaction_match_suggestions_unique").on( + table.inboxId, + table.transactionId, + ), + ], +); + +export const documentTagAssignments = pgTable( + "document_tag_assignments", + { + documentId: uuid("document_id").notNull(), + tagId: uuid("tag_id").notNull(), + teamId: uuid("team_id").notNull(), + }, + (table) => [ + index("idx_document_tag_assignments_document_id").using( + "btree", + table.documentId.asc().nullsLast().op("uuid_ops"), + ), + index("idx_document_tag_assignments_tag_id").using( + "btree", + table.tagId.asc().nullsLast().op("uuid_ops"), + ), + foreignKey({ + columns: [table.documentId], + foreignColumns: [documents.id], + name: "document_tag_assignments_document_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.tagId], + foreignColumns: [documentTags.id], + name: "document_tag_assignments_tag_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.teamId], + foreignColumns: [teams.id], + name: "document_tag_assignments_team_id_fkey", + }).onDelete("cascade"), + primaryKey({ + columns: [table.documentId, table.tagId], + name: "document_tag_assignments_pkey", + }), + unique("document_tag_assignments_unique").on(table.documentId, table.tagId), + pgPolicy("Tags can be handled by a member of the team", { + as: "permissive", + for: "all", + to: ["public"], + using: sql`(team_id IN ( SELECT private.get_teams_for_authenticated_user() AS get_teams_for_authenticated_user))`, + }), + ], +); + +export const usersOnTeam = pgTable( + "users_on_team", + { + userId: uuid("user_id").notNull(), + teamId: uuid("team_id").notNull(), + id: uuid().defaultRandom().notNull(), + role: teamRolesEnum(), + createdAt: timestamp("created_at", { + withTimezone: true, + mode: "string", + }).defaultNow(), + }, + (table) => [ + index("users_on_team_team_id_idx").using( + "btree", + table.teamId.asc().nullsLast().op("uuid_ops"), + ), + index("users_on_team_user_id_idx").using( + "btree", + table.userId.asc().nullsLast().op("uuid_ops"), + ), + foreignKey({ + columns: [table.teamId], + foreignColumns: [teams.id], + name: "users_on_team_team_id_fkey", + }) + .onUpdate("cascade") + .onDelete("cascade"), + foreignKey({ + columns: [table.userId], + foreignColumns: [users.id], + name: "users_on_team_user_id_fkey", + }).onDelete("cascade"), + primaryKey({ + columns: [table.userId, table.teamId, table.id], + name: "members_pkey", + }), + pgPolicy("Enable insert for authenticated users only", { + as: "permissive", + for: "insert", + to: ["authenticated"], + withCheck: sql`true`, + }), + pgPolicy("Enable updates for users on team", { + as: "permissive", + for: "update", + to: ["authenticated"], + }), + pgPolicy("Select for current user teams", { + as: "permissive", + for: "select", + to: ["authenticated"], + }), + pgPolicy("Users on team can be deleted by a member of the team", { + as: "permissive", + for: "delete", + to: ["public"], + }), + ], +); + +export const transactionCategories = pgTable( + "transaction_categories", + { + id: uuid().defaultRandom().notNull(), + name: text().notNull(), + teamId: uuid("team_id").notNull(), + color: text(), + createdAt: timestamp("created_at", { + withTimezone: true, + mode: "string", + }).defaultNow(), + system: boolean().default(false), + slug: text(), // Generated in database + taxRate: numericCasted("tax_rate", { precision: 10, scale: 2 }), + taxType: text("tax_type"), + taxReportingCode: text("tax_reporting_code"), + excluded: boolean("excluded").default(false), + description: text(), + parentId: uuid("parent_id"), + }, + (table) => [ + index("transaction_categories_team_id_idx").using( + "btree", + table.teamId.asc().nullsLast().op("uuid_ops"), + ), + index("transaction_categories_parent_id_idx").using( + "btree", + table.parentId.asc().nullsLast().op("uuid_ops"), + ), + foreignKey({ + columns: [table.teamId], + foreignColumns: [teams.id], + name: "transaction_categories_team_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.parentId], + foreignColumns: [table.id], + name: "transaction_categories_parent_id_fkey", + }).onDelete("set null"), + primaryKey({ + columns: [table.teamId, table.slug], + name: "transaction_categories_pkey", + }), + unique("unique_team_slug").on(table.teamId, table.slug), + pgPolicy("Users on team can manage categories", { + as: "permissive", + for: "all", + to: ["public"], + using: sql`(team_id IN ( SELECT private.get_teams_for_authenticated_user() AS get_teams_for_authenticated_user))`, + }), + ], +); + +export const usersInAuth = pgTable( + "auth.users", + { + instanceId: uuid("instance_id"), + id: uuid("id").notNull(), + aud: varchar("aud", { length: 255 }), + role: varchar("role", { length: 255 }), + email: varchar("email", { length: 255 }), + encryptedPassword: varchar("encrypted_password", { length: 255 }), + emailConfirmedAt: timestamp("email_confirmed_at", { withTimezone: true }), + invitedAt: timestamp("invited_at", { withTimezone: true }), + confirmationToken: varchar("confirmation_token", { length: 255 }), + confirmationSentAt: timestamp("confirmation_sent_at", { + withTimezone: true, + }), + recoveryToken: varchar("recovery_token", { length: 255 }), + recoverySentAt: timestamp("recovery_sent_at", { withTimezone: true }), + emailChangeTokenNew: varchar("email_change_token_new", { length: 255 }), + emailChange: varchar("email_change", { length: 255 }), + emailChangeSentAt: timestamp("email_change_sent_at", { + withTimezone: true, + }), + lastSignInAt: timestamp("last_sign_in_at", { withTimezone: true }), + rawAppMetaData: jsonb("raw_app_meta_data"), + rawUserMetaData: jsonb("raw_user_meta_data"), + isSuperAdmin: boolean("is_super_admin"), + createdAt: timestamp("created_at", { withTimezone: true }), + updatedAt: timestamp("updated_at", { withTimezone: true }), + phone: text("phone").default(sql`null::character varying`), + phoneConfirmedAt: timestamp("phone_confirmed_at", { withTimezone: true }), + phoneChange: text("phone_change").default(sql`''::character varying`), + phoneChangeToken: varchar("phone_change_token", { length: 255 }).default( + sql`''::character varying`, + ), + phoneChangeSentAt: timestamp("phone_change_sent_at", { + withTimezone: true, + }), + // Drizzle ORM does not support .stored() for generated columns, so we omit it + confirmedAt: timestamp("confirmed_at", { + withTimezone: true, + mode: "string", + }).generatedAlwaysAs(sql`LEAST(email_confirmed_at, phone_confirmed_at)`), + emailChangeTokenCurrent: varchar("email_change_token_current", { + length: 255, + }).default(sql`''::character varying`), + emailChangeConfirmStatus: smallint("email_change_confirm_status").default( + 0, + ), + bannedUntil: timestamp("banned_until", { withTimezone: true }), + reauthenticationToken: varchar("reauthentication_token", { + length: 255, + }).default(sql`''::character varying`), + reauthenticationSentAt: timestamp("reauthentication_sent_at", { + withTimezone: true, + }), + isSsoUser: boolean("is_sso_user").notNull().default(false), + deletedAt: timestamp("deleted_at", { withTimezone: true }), + isAnonymous: boolean("is_anonymous").notNull().default(false), + }, + (table) => [ + primaryKey({ columns: [table.id], name: "users_pkey" }), + unique("users_phone_key").on(table.phone), + unique("confirmation_token_idx").on(table.confirmationToken), + unique("email_change_token_current_idx").on(table.emailChangeTokenCurrent), + unique("email_change_token_new_idx").on(table.emailChangeTokenNew), + unique("reauthentication_token_idx").on(table.reauthenticationToken), + unique("recovery_token_idx").on(table.recoveryToken), + unique("users_email_partial_key").on(table.email), + index("users_instance_id_email_idx").on( + table.instanceId, + sql`lower((email)::text)`, + ), + index("users_instance_id_idx").on(table.instanceId), + index("users_is_anonymous_idx").on(table.isAnonymous), + // Check constraint for email_change_confirm_status + { + kind: "check", + name: "users_email_change_confirm_status_check", + expression: sql`((email_change_confirm_status >= 0) AND (email_change_confirm_status <= 2))`, + }, + ], +); + +export const shortLinks = pgTable( + "short_links", + { + id: uuid().defaultRandom().primaryKey().notNull(), + shortId: text("short_id").notNull(), + url: text().notNull(), + type: text("type"), + size: numericCasted("size", { precision: 10, scale: 2 }), + mimeType: text("mime_type"), + fileName: text("file_name"), + teamId: uuid("team_id").notNull(), + userId: uuid("user_id").notNull(), + expiresAt: timestamp("expires_at", { withTimezone: true, mode: "string" }), + createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) + .defaultNow() + .notNull(), + }, + (table) => [ + index("short_links_short_id_idx").using( + "btree", + table.shortId.asc().nullsLast().op("text_ops"), + ), + index("short_links_team_id_idx").using( + "btree", + table.teamId.asc().nullsLast().op("uuid_ops"), + ), + index("short_links_user_id_idx").using( + "btree", + table.userId.asc().nullsLast().op("uuid_ops"), + ), + foreignKey({ + columns: [table.userId], + foreignColumns: [users.id], + name: "short_links_user_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.teamId], + foreignColumns: [teams.id], + name: "short_links_team_id_fkey", + }).onDelete("cascade"), + unique("short_links_short_id_unique").on(table.shortId), + pgPolicy("Short links can be created by a member of the team", { + as: "permissive", + for: "insert", + to: ["authenticated"], + withCheck: sql`(team_id IN ( SELECT private.get_teams_for_authenticated_user() AS get_teams_for_authenticated_user))`, + }), + pgPolicy("Short links can be selected by a member of the team", { + as: "permissive", + for: "select", + to: ["authenticated"], + using: sql`(team_id IN ( SELECT private.get_teams_for_authenticated_user() AS get_teams_for_authenticated_user))`, + }), + pgPolicy("Short links can be updated by a member of the team", { + as: "permissive", + for: "update", + to: ["authenticated"], + using: sql`(team_id IN ( SELECT private.get_teams_for_authenticated_user() AS get_teams_for_authenticated_user))`, + }), + pgPolicy("Short links can be deleted by a member of the team", { + as: "permissive", + for: "delete", + to: ["authenticated"], + using: sql`(team_id IN ( SELECT private.get_teams_for_authenticated_user() AS get_teams_for_authenticated_user))`, + }), + ], +); + +export const apiKeys = pgTable( + "api_keys", + { + id: uuid("id").notNull().defaultRandom().primaryKey(), + keyEncrypted: text("key_encrypted").notNull(), + name: text("name").notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) + .notNull() + .defaultNow(), + userId: uuid("user_id").notNull(), + teamId: uuid("team_id").notNull(), + keyHash: text("key_hash"), + scopes: text("scopes").array().notNull().default(sql`'{}'::text[]`), + lastUsedAt: timestamp("last_used_at", { + withTimezone: true, + mode: "string", + }), + }, + (table) => [ + index("api_keys_key_idx").using( + "btree", + table.keyHash.asc().nullsLast().op("text_ops"), + ), + index("api_keys_user_id_idx").using( + "btree", + table.userId.asc().nullsLast().op("uuid_ops"), + ), + index("api_keys_team_id_idx").using( + "btree", + table.teamId.asc().nullsLast().op("uuid_ops"), + ), + foreignKey({ + columns: [table.userId], + foreignColumns: [users.id], + name: "api_keys_user_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.teamId], + foreignColumns: [teams.id], + name: "api_keys_team_id_fkey", + }).onDelete("cascade"), + unique("api_keys_key_unique").on(table.keyHash), + ], +); + +// Relations +// OAuth Applications +export const oauthApplications = pgTable( + "oauth_applications", + { + id: uuid("id").notNull().defaultRandom().primaryKey(), + name: text("name").notNull(), + slug: text("slug").notNull().unique(), + description: text("description"), + overview: text("overview"), + developerName: text("developer_name"), + logoUrl: text("logo_url"), + website: text("website"), + installUrl: text("install_url"), + screenshots: text("screenshots").array().default(sql`'{}'::text[]`), + redirectUris: text("redirect_uris").array().notNull(), + clientId: text("client_id").notNull().unique(), + clientSecret: text("client_secret").notNull(), + scopes: text("scopes").array().notNull().default(sql`'{}'::text[]`), + teamId: uuid("team_id").notNull(), + createdBy: uuid("created_by").notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) + .notNull() + .defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true, mode: "string" }) + .notNull() + .defaultNow(), + isPublic: boolean("is_public").default(false), + active: boolean("active").default(true), + status: text("status", { + enum: ["draft", "pending", "approved", "rejected"], + }).default("draft"), + }, + (table) => [ + index("oauth_applications_team_id_idx").using( + "btree", + table.teamId.asc().nullsLast().op("uuid_ops"), + ), + index("oauth_applications_client_id_idx").using( + "btree", + table.clientId.asc().nullsLast().op("text_ops"), + ), + index("oauth_applications_slug_idx").using( + "btree", + table.slug.asc().nullsLast().op("text_ops"), + ), + foreignKey({ + columns: [table.teamId], + foreignColumns: [teams.id], + name: "oauth_applications_team_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.createdBy], + foreignColumns: [users.id], + name: "oauth_applications_created_by_fkey", + }).onDelete("cascade"), + pgPolicy("OAuth applications can be managed by team members", { + as: "permissive", + for: "all", + to: ["public"], + using: sql`(team_id IN ( SELECT private.get_teams_for_authenticated_user() AS get_teams_for_authenticated_user))`, + }), + ], +); + +// OAuth Authorization Codes +export const oauthAuthorizationCodes = pgTable( + "oauth_authorization_codes", + { + id: uuid("id").notNull().defaultRandom().primaryKey(), + code: text("code").notNull().unique(), + applicationId: uuid("application_id").notNull(), + userId: uuid("user_id").notNull(), + teamId: uuid("team_id").notNull(), + scopes: text("scopes").array().notNull(), + redirectUri: text("redirect_uri").notNull(), + expiresAt: timestamp("expires_at", { + withTimezone: true, + mode: "string", + }).notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) + .notNull() + .defaultNow(), + used: boolean("used").default(false), + codeChallenge: text("code_challenge"), + codeChallengeMethod: text("code_challenge_method"), + }, + (table) => [ + index("oauth_authorization_codes_code_idx").using( + "btree", + table.code.asc().nullsLast().op("text_ops"), + ), + index("oauth_authorization_codes_application_id_idx").using( + "btree", + table.applicationId.asc().nullsLast().op("uuid_ops"), + ), + index("oauth_authorization_codes_user_id_idx").using( + "btree", + table.userId.asc().nullsLast().op("uuid_ops"), + ), + foreignKey({ + columns: [table.applicationId], + foreignColumns: [oauthApplications.id], + name: "oauth_authorization_codes_application_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.userId], + foreignColumns: [users.id], + name: "oauth_authorization_codes_user_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.teamId], + foreignColumns: [teams.id], + name: "oauth_authorization_codes_team_id_fkey", + }).onDelete("cascade"), + ], +); + +// OAuth Access Tokens +export const oauthAccessTokens = pgTable( + "oauth_access_tokens", + { + id: uuid("id").notNull().defaultRandom().primaryKey(), + token: text("token").notNull().unique(), + refreshToken: text("refresh_token").unique(), + applicationId: uuid("application_id").notNull(), + userId: uuid("user_id").notNull(), + teamId: uuid("team_id").notNull(), + scopes: text("scopes").array().notNull(), + expiresAt: timestamp("expires_at", { + withTimezone: true, + mode: "string", + }).notNull(), + refreshTokenExpiresAt: timestamp("refresh_token_expires_at", { + withTimezone: true, + mode: "string", + }), + createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) + .notNull() + .defaultNow(), + lastUsedAt: timestamp("last_used_at", { + withTimezone: true, + mode: "string", + }), + revoked: boolean("revoked").default(false), + revokedAt: timestamp("revoked_at", { withTimezone: true, mode: "string" }), + }, + (table) => [ + index("oauth_access_tokens_token_idx").using( + "btree", + table.token.asc().nullsLast().op("text_ops"), + ), + index("oauth_access_tokens_refresh_token_idx").using( + "btree", + table.refreshToken.asc().nullsLast().op("text_ops"), + ), + index("oauth_access_tokens_application_id_idx").using( + "btree", + table.applicationId.asc().nullsLast().op("uuid_ops"), + ), + index("oauth_access_tokens_user_id_idx").using( + "btree", + table.userId.asc().nullsLast().op("uuid_ops"), + ), + foreignKey({ + columns: [table.applicationId], + foreignColumns: [oauthApplications.id], + name: "oauth_access_tokens_application_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.userId], + foreignColumns: [users.id], + name: "oauth_access_tokens_user_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.teamId], + foreignColumns: [teams.id], + name: "oauth_access_tokens_team_id_fkey", + }).onDelete("cascade"), + ], +); + +export const transactionsRelations = relations( + transactions, + ({ one, many }) => ({ + user: one(users, { + fields: [transactions.assignedId], + references: [users.id], + }), + team: one(teams, { + fields: [transactions.teamId], + references: [teams.id], + }), + bankAccount: one(bankAccounts, { + fields: [transactions.bankAccountId], + references: [bankAccounts.id], + }), + transactionCategory: one(transactionCategories, { + fields: [transactions.teamId], + references: [transactionCategories.teamId], + }), + transactionTags: many(transactionTags), + transactionAttachments: many(transactionAttachments), + inboxes: many(inbox), + }), +); + +export const usersRelations = relations(users, ({ one, many }) => ({ + transactions: many(transactions), + trackerEntries: many(trackerEntries), + bankAccounts: many(bankAccounts), + invoices: many(invoices), + trackerReports: many(trackerReports), + reports: many(reports), + userInvites: many(userInvites), + documents: many(documents), + apps: many(apps), + apiKeys: many(apiKeys), + shortLinks: many(shortLinks), + oauthApplications: many(oauthApplications), + oauthAuthorizationCodes: many(oauthAuthorizationCodes), + oauthAccessTokens: many(oauthAccessTokens), + usersInAuth: one(usersInAuth, { + fields: [users.id], + references: [usersInAuth.id], + }), + team: one(teams, { + fields: [users.teamId], + references: [teams.id], + }), + usersOnTeams: many(usersOnTeam), +})); + +export const shortLinksRelations = relations(shortLinks, ({ one }) => ({ + user: one(users, { + fields: [shortLinks.userId], + references: [users.id], + }), + team: one(teams, { + fields: [shortLinks.teamId], + references: [teams.id], + }), +})); + +export const apiKeysRelations = relations(apiKeys, ({ one }) => ({ + user: one(users, { + fields: [apiKeys.userId], + references: [users.id], + }), + team: one(teams, { + fields: [apiKeys.teamId], + references: [teams.id], + }), +})); + +export const teamsRelations = relations(teams, ({ many }) => ({ + transactions: many(transactions), + trackerEntries: many(trackerEntries), + customerTags: many(customerTags), + inboxAccounts: many(inboxAccounts), + bankAccounts: many(bankAccounts), + invoices: many(invoices), + customers: many(customers), + tags: many(tags), + trackerReports: many(trackerReports), + trackerProjectTags: many(trackerProjectTags), + reports: many(reports), + bankConnections: many(bankConnections), + userInvites: many(userInvites), + documentTags: many(documentTags), + transactionTags: many(transactionTags), + transactionAttachments: many(transactionAttachments), + documents: many(documents), + apps: many(apps), + apiKeys: many(apiKeys), + shortLinks: many(shortLinks), + invoiceTemplates: many(invoiceTemplates), + transactionEnrichments: many(transactionEnrichments), + users: many(users), + trackerProjects: many(trackerProjects), + inboxes: many(inbox), + documentTagAssignments: many(documentTagAssignments), + usersOnTeams: many(usersOnTeam), + transactionCategories: many(transactionCategories), +})); + +export const bankAccountsRelations = relations( + bankAccounts, + ({ one, many }) => ({ + transactions: many(transactions), + bankConnection: one(bankConnections, { + fields: [bankAccounts.bankConnectionId], + references: [bankConnections.id], + }), + user: one(users, { + fields: [bankAccounts.createdBy], + references: [users.id], + }), + team: one(teams, { + fields: [bankAccounts.teamId], + references: [teams.id], + }), + }), +); + +export const transactionCategoriesRelations = relations( + transactionCategories, + ({ one, many }) => ({ + transactions: many(transactions), + transactionEnrichments: many(transactionEnrichments), + team: one(teams, { + fields: [transactionCategories.teamId], + references: [teams.id], + }), + parent: one(transactionCategories, { + fields: [transactionCategories.parentId], + references: [transactionCategories.id], + relationName: "parent_child", + }), + children: many(transactionCategories, { + relationName: "parent_child", + }), + }), +); + +export const trackerEntriesRelations = relations(trackerEntries, ({ one }) => ({ + user: one(users, { + fields: [trackerEntries.assignedId], + references: [users.id], + }), + trackerProject: one(trackerProjects, { + fields: [trackerEntries.projectId], + references: [trackerProjects.id], + }), + team: one(teams, { + fields: [trackerEntries.teamId], + references: [teams.id], + }), +})); + +export const trackerProjectsRelations = relations( + trackerProjects, + ({ one, many }) => ({ + trackerEntries: many(trackerEntries), + trackerReports: many(trackerReports), + trackerProjectTags: many(trackerProjectTags), + customer: one(customers, { + fields: [trackerProjects.customerId], + references: [customers.id], + }), + team: one(teams, { + fields: [trackerProjects.teamId], + references: [teams.id], + }), + }), +); + +export const customerTagsRelations = relations(customerTags, ({ one }) => ({ + customer: one(customers, { + fields: [customerTags.customerId], + references: [customers.id], + }), + tag: one(tags, { + fields: [customerTags.tagId], + references: [tags.id], + }), + team: one(teams, { + fields: [customerTags.teamId], + references: [teams.id], + }), +})); + +export const customersRelations = relations(customers, ({ one, many }) => ({ + customerTags: many(customerTags), + invoices: many(invoices), + team: one(teams, { + fields: [customers.teamId], + references: [teams.id], + }), + trackerProjects: many(trackerProjects), +})); + +export const tagsRelations = relations(tags, ({ one, many }) => ({ + customerTags: many(customerTags), + team: one(teams, { + fields: [tags.teamId], + references: [teams.id], + }), + trackerProjectTags: many(trackerProjectTags), + transactionTags: many(transactionTags), +})); + +export const inboxAccountsRelations = relations(inboxAccounts, ({ one }) => ({ + team: one(teams, { + fields: [inboxAccounts.teamId], + references: [teams.id], + }), +})); + +export const bankConnectionsRelations = relations( + bankConnections, + ({ one, many }) => ({ + bankAccounts: many(bankAccounts), + team: one(teams, { + fields: [bankConnections.teamId], + references: [teams.id], + }), + }), +); + +export const invoicesRelations = relations(invoices, ({ one }) => ({ + user: one(users, { + fields: [invoices.userId], + references: [users.id], + }), + customer: one(customers, { + fields: [invoices.customerId], + references: [customers.id], + }), + team: one(teams, { + fields: [invoices.teamId], + references: [teams.id], + }), +})); + +export const trackerReportsRelations = relations(trackerReports, ({ one }) => ({ + user: one(users, { + fields: [trackerReports.createdBy], + references: [users.id], + }), + trackerProject: one(trackerProjects, { + fields: [trackerReports.projectId], + references: [trackerProjects.id], + }), + team: one(teams, { + fields: [trackerReports.teamId], + references: [teams.id], + }), +})); + +export const trackerProjectTagsRelations = relations( + trackerProjectTags, + ({ one }) => ({ + tag: one(tags, { + fields: [trackerProjectTags.tagId], + references: [tags.id], + }), + trackerProject: one(trackerProjects, { + fields: [trackerProjectTags.trackerProjectId], + references: [trackerProjects.id], + }), + team: one(teams, { + fields: [trackerProjectTags.teamId], + references: [teams.id], + }), + }), +); + +export const reportsRelations = relations(reports, ({ one }) => ({ + user: one(users, { + fields: [reports.createdBy], + references: [users.id], + }), + team: one(teams, { + fields: [reports.teamId], + references: [teams.id], + }), +})); + +export const userInvitesRelations = relations(userInvites, ({ one }) => ({ + team: one(teams, { + fields: [userInvites.teamId], + references: [teams.id], + }), + user: one(users, { + fields: [userInvites.invitedBy], + references: [users.id], + }), +})); + +export const documentTagsRelations = relations( + documentTags, + ({ one, many }) => ({ + team: one(teams, { + fields: [documentTags.teamId], + references: [teams.id], + }), + documentTagAssignments: many(documentTagAssignments), + }), +); + +export const transactionTagsRelations = relations( + transactionTags, + ({ one }) => ({ + tag: one(tags, { + fields: [transactionTags.tagId], + references: [tags.id], + }), + team: one(teams, { + fields: [transactionTags.teamId], + references: [teams.id], + }), + transaction: one(transactions, { + fields: [transactionTags.transactionId], + references: [transactions.id], + }), + }), +); + +export const transactionAttachmentsRelations = relations( + transactionAttachments, + ({ one, many }) => ({ + team: one(teams, { + fields: [transactionAttachments.teamId], + references: [teams.id], + }), + transaction: one(transactions, { + fields: [transactionAttachments.transactionId], + references: [transactions.id], + }), + inboxes: many(inbox), + }), +); + +export const documentsRelations = relations(documents, ({ one, many }) => ({ + user: one(users, { + fields: [documents.ownerId], + references: [users.id], + }), + team: one(teams, { + fields: [documents.teamId], + references: [teams.id], + }), + documentTagAssignments: many(documentTagAssignments), +})); + +export const appsRelations = relations(apps, ({ one }) => ({ + user: one(users, { + fields: [apps.createdBy], + references: [users.id], + }), + team: one(teams, { + fields: [apps.teamId], + references: [teams.id], + }), +})); + +export const invoiceTemplatesRelations = relations( + invoiceTemplates, + ({ one }) => ({ + team: one(teams, { + fields: [invoiceTemplates.teamId], + references: [teams.id], + }), + }), +); + +export const transactionEnrichmentsRelations = relations( + transactionEnrichments, + ({ one }) => ({ + transactionCategory: one(transactionCategories, { + fields: [transactionEnrichments.teamId], + references: [transactionCategories.teamId], + }), + team: one(teams, { + fields: [transactionEnrichments.teamId], + references: [teams.id], + }), + }), +); + +export const usersInAuthRelations = relations(usersInAuth, ({ many }) => ({ + users: many(users), +})); + +export const inboxRelations = relations(inbox, ({ one }) => ({ + transactionAttachment: one(transactionAttachments, { + fields: [inbox.attachmentId], + references: [transactionAttachments.id], + }), + team: one(teams, { + fields: [inbox.teamId], + references: [teams.id], + }), + transaction: one(transactions, { + fields: [inbox.transactionId], + references: [transactions.id], + }), +})); + +export const documentTagAssignmentsRelations = relations( + documentTagAssignments, + ({ one }) => ({ + document: one(documents, { + fields: [documentTagAssignments.documentId], + references: [documents.id], + }), + documentTag: one(documentTags, { + fields: [documentTagAssignments.tagId], + references: [documentTags.id], + }), + team: one(teams, { + fields: [documentTagAssignments.teamId], + references: [teams.id], + }), + }), +); + +export const usersOnTeamRelations = relations(usersOnTeam, ({ one }) => ({ + team: one(teams, { + fields: [usersOnTeam.teamId], + references: [teams.id], + }), + user: one(users, { + fields: [usersOnTeam.userId], + references: [users.id], + }), +})); + +// OAuth Relations +export const oauthApplicationsRelations = relations( + oauthApplications, + ({ one, many }) => ({ + team: one(teams, { + fields: [oauthApplications.teamId], + references: [teams.id], + }), + createdBy: one(users, { + fields: [oauthApplications.createdBy], + references: [users.id], + }), + authorizationCodes: many(oauthAuthorizationCodes), + accessTokens: many(oauthAccessTokens), + }), +); + +export const oauthAuthorizationCodesRelations = relations( + oauthAuthorizationCodes, + ({ one }) => ({ + application: one(oauthApplications, { + fields: [oauthAuthorizationCodes.applicationId], + references: [oauthApplications.id], + }), + user: one(users, { + fields: [oauthAuthorizationCodes.userId], + references: [users.id], + }), + team: one(teams, { + fields: [oauthAuthorizationCodes.teamId], + references: [teams.id], + }), + }), +); + +export const oauthAccessTokensRelations = relations( + oauthAccessTokens, + ({ one }) => ({ + application: one(oauthApplications, { + fields: [oauthAccessTokens.applicationId], + references: [oauthApplications.id], + }), + user: one(users, { + fields: [oauthAccessTokens.userId], + references: [users.id], + }), + team: one(teams, { + fields: [oauthAccessTokens.teamId], + references: [teams.id], + }), + }), +); + +export const activities = pgTable( + "activities", + { + id: uuid().defaultRandom().primaryKey().notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) + .defaultNow() + .notNull(), + + // Core fields + teamId: uuid("team_id").notNull(), + userId: uuid("user_id"), + type: activityTypeEnum().notNull(), + priority: smallint().default(5), // 1-3 = notifications, 4-10 = insights only + + // Group related activities together (e.g., same business event across multiple users) + groupId: uuid("group_id"), + + // Source of the activity + source: activitySourceEnum().notNull(), + + // All the data + metadata: jsonb().notNull(), + + // Simple lifecycle (only for notifications) + status: activityStatusEnum().default("unread").notNull(), + + // Timestamp of last system use (e.g. insight generation, digest inclusion) + lastUsedAt: timestamp("last_used_at", { + withTimezone: true, + mode: "string", + }), + }, + (table) => [ + // Optimized indexes + index("activities_notifications_idx").using( + "btree", + table.teamId, + table.priority, + table.status, + table.createdAt.desc(), + ), + index("activities_insights_idx").using( + "btree", + table.teamId, + table.type, + table.source, + table.createdAt.desc(), + ), + index("activities_metadata_gin_idx").using("gin", table.metadata), + index("activities_group_id_idx").on(table.groupId), + index("activities_insights_group_idx").using( + "btree", + table.teamId, + table.groupId, + table.type, + table.createdAt.desc(), + ), + + // Foreign keys + foreignKey({ + columns: [table.teamId], + foreignColumns: [teams.id], + name: "activities_team_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.userId], + foreignColumns: [users.id], + name: "activities_user_id_fkey", + }).onDelete("set null"), + ], +); + +export const notificationSettings = pgTable( + "notification_settings", + { + id: uuid().defaultRandom().primaryKey().notNull(), + userId: uuid("user_id").notNull(), + teamId: uuid("team_id").notNull(), + notificationType: text("notification_type").notNull(), + channel: text("channel").notNull(), // 'in_app', 'email', 'push' + enabled: boolean().default(true).notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) + .defaultNow() + .notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true, mode: "string" }) + .defaultNow() + .notNull(), + }, + (table) => [ + unique("notification_settings_user_team_type_channel_key").on( + table.userId, + table.teamId, + table.notificationType, + table.channel, + ), + index("notification_settings_user_team_idx").on(table.userId, table.teamId), + index("notification_settings_type_channel_idx").on( + table.notificationType, + table.channel, + ), + foreignKey({ + columns: [table.userId], + foreignColumns: [users.id], + name: "notification_settings_user_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.teamId], + foreignColumns: [teams.id], + name: "notification_settings_team_id_fkey", + }).onDelete("cascade"), + pgPolicy("Users can manage their own notification settings", { + as: "permissive", + for: "all", + to: ["public"], + using: sql`(user_id = auth.uid())`, + }), + ], +); diff --git a/basango/packages/db/src/utils/api-keys.ts b/basango/packages/db/src/utils/api-keys.ts new file mode 100644 index 0000000..7504f0c --- /dev/null +++ b/basango/packages/db/src/utils/api-keys.ts @@ -0,0 +1,20 @@ +import { randomBytes } from "node:crypto"; + +/** + * Generates a new API key with the format mid_{random_string} + * @returns A new API key string + */ +export function generateApiKey(): string { + // Generate 32 random bytes and convert to hex + const randomString = randomBytes(32).toString("hex"); + return `mid_${randomString}`; +} + +/** + * Validates if a string is a valid API key format + * @param key The key to validate + * @returns True if the key starts with 'mid-' and has the correct length + */ +export function isValidApiKeyFormat(key: string): boolean { + return key.startsWith("mid_") && key.length === 68; // mid_ (4) + 64 hex chars +} diff --git a/basango/.npmrc b/basango/packages/db/src/utils/health.ts similarity index 100% rename from basango/.npmrc rename to basango/packages/db/src/utils/health.ts diff --git a/basango/packages/db/src/utils/search-query.ts b/basango/packages/db/src/utils/search-query.ts new file mode 100644 index 0000000..06a0a7f --- /dev/null +++ b/basango/packages/db/src/utils/search-query.ts @@ -0,0 +1,16 @@ +export const buildSearchQuery = (input: string) => { + const trimmed = input.trim(); + if (!trimmed) { + return ""; + } + + return trimmed + .split(/\s+/) + .map((term) => { + // Escape special characters for PostgreSQL full-text search + // Special characters: & | ! ( ) : * ' " + - ~ + const escaped = term.toLowerCase().replace(/[&|!():*'"+~-]/g, "\\$&"); + return `${escaped}:*`; + }) + .join(" & "); +}; diff --git a/basango/packages/db/tsconfig.json b/basango/packages/db/tsconfig.json new file mode 100644 index 0000000..d100dfe --- /dev/null +++ b/basango/packages/db/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@midday/tsconfig/base.json", + "include": ["src"], + "exclude": ["node_modules"], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/db": ["./src/*"] + } + } +} \ No newline at end of file diff --git a/basango/packages/eslint-config/README.md b/basango/packages/eslint-config/README.md deleted file mode 100644 index 8b42d90..0000000 --- a/basango/packages/eslint-config/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# `@turbo/eslint-config` - -Collection of internal eslint configurations. diff --git a/basango/packages/eslint-config/base.js b/basango/packages/eslint-config/base.js deleted file mode 100644 index 09d316e..0000000 --- a/basango/packages/eslint-config/base.js +++ /dev/null @@ -1,32 +0,0 @@ -import js from "@eslint/js"; -import eslintConfigPrettier from "eslint-config-prettier"; -import turboPlugin from "eslint-plugin-turbo"; -import tseslint from "typescript-eslint"; -import onlyWarn from "eslint-plugin-only-warn"; - -/** - * A shared ESLint configuration for the repository. - * - * @type {import("eslint").Linter.Config[]} - * */ -export const config = [ - js.configs.recommended, - eslintConfigPrettier, - ...tseslint.configs.recommended, - { - plugins: { - turbo: turboPlugin, - }, - rules: { - "turbo/no-undeclared-env-vars": "warn", - }, - }, - { - plugins: { - onlyWarn, - }, - }, - { - ignores: ["dist/**"], - }, -]; diff --git a/basango/packages/eslint-config/next.js b/basango/packages/eslint-config/next.js deleted file mode 100644 index 4df088a..0000000 --- a/basango/packages/eslint-config/next.js +++ /dev/null @@ -1,57 +0,0 @@ -import js from "@eslint/js"; -import { globalIgnores } from "eslint/config"; -import eslintConfigPrettier from "eslint-config-prettier"; -import tseslint from "typescript-eslint"; -import pluginReactHooks from "eslint-plugin-react-hooks"; -import pluginReact from "eslint-plugin-react"; -import globals from "globals"; -import pluginNext from "@next/eslint-plugin-next"; -import { config as baseConfig } from "./base.js"; - -/** - * A custom ESLint configuration for libraries that use Next.js. - * - * @type {import("eslint").Linter.Config[]} - * */ -export const nextJsConfig = [ - ...baseConfig, - js.configs.recommended, - eslintConfigPrettier, - ...tseslint.configs.recommended, - globalIgnores([ - // Default ignores of eslint-config-next: - ".next/**", - "out/**", - "build/**", - "next-env.d.ts", - ]), - { - ...pluginReact.configs.flat.recommended, - languageOptions: { - ...pluginReact.configs.flat.recommended.languageOptions, - globals: { - ...globals.serviceworker, - }, - }, - }, - { - plugins: { - "@next/next": pluginNext, - }, - rules: { - ...pluginNext.configs.recommended.rules, - ...pluginNext.configs["core-web-vitals"].rules, - }, - }, - { - plugins: { - "react-hooks": pluginReactHooks, - }, - settings: { react: { version: "detect" } }, - rules: { - ...pluginReactHooks.configs.recommended.rules, - // React scope no longer necessary with new JSX transform. - "react/react-in-jsx-scope": "off", - }, - }, -]; diff --git a/basango/packages/eslint-config/package.json b/basango/packages/eslint-config/package.json deleted file mode 100644 index c8ed81d..0000000 --- a/basango/packages/eslint-config/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "@basango/eslint-config", - "version": "0.0.0", - "type": "module", - "private": true, - "exports": { - "./base": "./base.js", - "./next-js": "./next.js", - "./react-internal": "./react-internal.js" - }, - "devDependencies": { - "@eslint/js": "^9.34.0", - "@next/eslint-plugin-next": "^15.5.0", - "eslint": "^9.38.0", - "eslint-config-prettier": "^10.1.1", - "eslint-plugin-only-warn": "^1.1.0", - "eslint-plugin-react": "^7.37.5", - "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-turbo": "^2.5.0", - "globals": "^16.3.0", - "typescript": "^5.9.2", - "typescript-eslint": "^8.40.0" - } -} diff --git a/basango/packages/eslint-config/react-internal.js b/basango/packages/eslint-config/react-internal.js deleted file mode 100644 index daeccba..0000000 --- a/basango/packages/eslint-config/react-internal.js +++ /dev/null @@ -1,39 +0,0 @@ -import js from "@eslint/js"; -import eslintConfigPrettier from "eslint-config-prettier"; -import tseslint from "typescript-eslint"; -import pluginReactHooks from "eslint-plugin-react-hooks"; -import pluginReact from "eslint-plugin-react"; -import globals from "globals"; -import { config as baseConfig } from "./base.js"; - -/** - * A custom ESLint configuration for libraries that use React. - * - * @type {import("eslint").Linter.Config[]} */ -export const config = [ - ...baseConfig, - js.configs.recommended, - eslintConfigPrettier, - ...tseslint.configs.recommended, - pluginReact.configs.flat.recommended, - { - languageOptions: { - ...pluginReact.configs.flat.recommended.languageOptions, - globals: { - ...globals.serviceworker, - ...globals.browser, - }, - }, - }, - { - plugins: { - "react-hooks": pluginReactHooks, - }, - settings: { react: { version: "detect" } }, - rules: { - ...pluginReactHooks.configs.recommended.rules, - // React scope no longer necessary with new JSX transform. - "react/react-in-jsx-scope": "off", - }, - }, -]; diff --git a/basango/packages/logger/package.json b/basango/packages/logger/package.json new file mode 100644 index 0000000..7d27b2c --- /dev/null +++ b/basango/packages/logger/package.json @@ -0,0 +1,18 @@ +{ + "name": "@basango/logger", + "version": "0.0.1", + "private": true, + "type": "module", + "main": "src/index.ts", + "types": "src/index.ts", + "scripts": { + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "typescript": "^5.9.2" + }, + "dependencies": { + "pino": "^10.1.0", + "pino-pretty": "^13.1.2" + } +} diff --git a/basango/packages/logger/src/index.ts b/basango/packages/logger/src/index.ts new file mode 100644 index 0000000..967c42e --- /dev/null +++ b/basango/packages/logger/src/index.ts @@ -0,0 +1,20 @@ +import pino from "pino"; + +export const logger = pino({ + level: process.env.LOG_LEVEL || "info", + // Use pretty printing in development, structured JSON in production + ...(process.env.NODE_ENV === "development" && { + transport: { + target: "pino-pretty", + options: { + colorize: true, + translateTime: "HH:MM:ss", + ignore: "pid,hostname", + messageFormat: true, + hideObject: false, + }, + }, + }), +}); + +export default logger; diff --git a/basango/packages/logger/tsconfig.json b/basango/packages/logger/tsconfig.json new file mode 100644 index 0000000..85bb54e --- /dev/null +++ b/basango/packages/logger/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "@basango/tsconfig/base.json", + "include": ["src/**/*"], + "exclude": ["node_modules"], +} diff --git a/basango/packages/tsconfig/base.json b/basango/packages/tsconfig/base.json new file mode 100644 index 0000000..0756a8c --- /dev/null +++ b/basango/packages/tsconfig/base.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "incremental": false, + "isolatedModules": true, + "lib": ["es2022", "DOM", "DOM.Iterable"], + "module": "NodeNext", + "moduleDetection": "force", + "moduleResolution": "NodeNext", + "noUncheckedIndexedAccess": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "ES2022" + } +} diff --git a/basango/packages/tsconfig/nextjs.json b/basango/packages/tsconfig/nextjs.json new file mode 100644 index 0000000..20317a2 --- /dev/null +++ b/basango/packages/tsconfig/nextjs.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "./base.json", + "compilerOptions": { + "plugins": [{ "name": "next" }], + "module": "ESNext", + "moduleResolution": "Bundler", + "allowJs": true, + "jsx": "preserve", + "noEmit": true + } +} diff --git a/basango/packages/tsconfig/package.json b/basango/packages/tsconfig/package.json new file mode 100644 index 0000000..a91647e --- /dev/null +++ b/basango/packages/tsconfig/package.json @@ -0,0 +1,12 @@ +{ + "name": "@basango/tsconfig", + "version": "0.0.0", + "private": true, + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "files": [ + "base.json" + ] +} diff --git a/basango/packages/tsconfig/react-library.json b/basango/packages/tsconfig/react-library.json new file mode 100644 index 0000000..44957d6 --- /dev/null +++ b/basango/packages/tsconfig/react-library.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "./base.json", + "compilerOptions": { + "jsx": "react-jsx" + } +} diff --git a/basango/packages/typescript-config/base.json b/basango/packages/typescript-config/base.json deleted file mode 100644 index 5117f2a..0000000 --- a/basango/packages/typescript-config/base.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig", - "compilerOptions": { - "declaration": true, - "declarationMap": true, - "esModuleInterop": true, - "incremental": false, - "isolatedModules": true, - "lib": ["es2022", "DOM", "DOM.Iterable"], - "module": "NodeNext", - "moduleDetection": "force", - "moduleResolution": "NodeNext", - "noUncheckedIndexedAccess": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "strict": true, - "target": "ES2022" - } -} diff --git a/basango/packages/typescript-config/nextjs.json b/basango/packages/typescript-config/nextjs.json deleted file mode 100644 index e6defa4..0000000 --- a/basango/packages/typescript-config/nextjs.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig", - "extends": "./base.json", - "compilerOptions": { - "plugins": [{ "name": "next" }], - "module": "ESNext", - "moduleResolution": "Bundler", - "allowJs": true, - "jsx": "preserve", - "noEmit": true - } -} diff --git a/basango/packages/typescript-config/package.json b/basango/packages/typescript-config/package.json deleted file mode 100644 index 34e19d4..0000000 --- a/basango/packages/typescript-config/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "@basango/typescript-config", - "version": "0.0.0", - "private": true, - "license": "MIT", - "publishConfig": { - "access": "public" - } -} diff --git a/basango/packages/typescript-config/react-library.json b/basango/packages/typescript-config/react-library.json deleted file mode 100644 index c3a1b26..0000000 --- a/basango/packages/typescript-config/react-library.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig", - "extends": "./base.json", - "compilerOptions": { - "jsx": "react-jsx" - } -} diff --git a/basango/packages/ui/eslint.config.mjs b/basango/packages/ui/eslint.config.mjs deleted file mode 100644 index 19170f8..0000000 --- a/basango/packages/ui/eslint.config.mjs +++ /dev/null @@ -1,4 +0,0 @@ -import { config } from "@repo/eslint-config/react-internal"; - -/** @type {import("eslint").Linter.Config} */ -export default config; diff --git a/basango/packages/ui/package.json b/basango/packages/ui/package.json deleted file mode 100644 index c5ce589..0000000 --- a/basango/packages/ui/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "@basango/ui", - "version": "0.0.0", - "private": true, - "exports": { - "./*": "./src/*.tsx" - }, - "scripts": { - "lint": "eslint . --max-warnings 0", - "generate:component": "turbo gen react-component", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@basango/eslint-config": "*", - "@basango/typescript-config": "*", - "@types/node": "^22.15.3", - "@types/react": "19.1.0", - "@types/react-dom": "19.1.1", - "eslint": "^9.38.0", - "typescript": "5.9.2" - }, - "dependencies": { - "react": "^19.2.0", - "react-dom": "^19.1.0" - } -} diff --git a/basango/packages/ui/src/button.tsx b/basango/packages/ui/src/button.tsx deleted file mode 100644 index 78e5420..0000000 --- a/basango/packages/ui/src/button.tsx +++ /dev/null @@ -1,20 +0,0 @@ -"use client"; - -import { ReactNode } from "react"; - -interface ButtonProps { - children: ReactNode; - className?: string; - appName: string; -} - -export const Button = ({ children, className, appName }: ButtonProps) => { - return ( - - ); -}; diff --git a/basango/packages/ui/src/card.tsx b/basango/packages/ui/src/card.tsx deleted file mode 100644 index 7b98893..0000000 --- a/basango/packages/ui/src/card.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { type JSX } from "react"; - -export function Card({ - className, - title, - children, - href, -}: { - className?: string; - title: string; - children: React.ReactNode; - href: string; -}): JSX.Element { - return ( - -

- {title} -> -

-

{children}

-
- ); -} diff --git a/basango/packages/ui/src/code.tsx b/basango/packages/ui/src/code.tsx deleted file mode 100644 index f7cbd22..0000000 --- a/basango/packages/ui/src/code.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { type JSX } from "react"; - -export function Code({ - children, - className, -}: { - children: React.ReactNode; - className?: string; -}): JSX.Element { - return {children}; -} diff --git a/basango/packages/ui/tsconfig.json b/basango/packages/ui/tsconfig.json deleted file mode 100644 index ca86687..0000000 --- a/basango/packages/ui/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "@repo/typescript-config/react-library.json", - "compilerOptions": { - "outDir": "dist" - }, - "include": ["src"], - "exclude": ["node_modules", "dist"] -} diff --git a/basango/tsconfig.json b/basango/tsconfig.json new file mode 100644 index 0000000..4d296f6 --- /dev/null +++ b/basango/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "@basango/tsconfig/base.json" +} diff --git a/basango/turbo.json b/basango/turbo.json index d508947..45f4415 100644 --- a/basango/turbo.json +++ b/basango/turbo.json @@ -1,21 +1,43 @@ { - "$schema": "https://turborepo.com/schema.json", - "ui": "tui", - "tasks": { - "build": { - "dependsOn": ["^build"], - "inputs": ["$TURBO_DEFAULT$", ".env*"], - "outputs": [".next/**", "!.next/cache/**"] - }, - "lint": { - "dependsOn": ["^lint"] - }, - "check-types": { - "dependsOn": ["^check-types"] - }, - "dev": { - "cache": false, - "persistent": true - } - } + "$schema": "https://turborepo.com/schema.json", + "globalDependencies": ["**/.env"], + "ui": "tui", + "tasks": { + "topo": { + "dependsOn": ["^topo"] + }, + "build": { + "dependsOn": ["^build"], + "inputs": ["$TURBO_DEFAULT$", ".env*"], + "outputs": [ + ".next/**", + "!.next/cache/**", + "next-env.d.ts", + ".expo/**", + "dist/**", + "build/**", + "lib/**" + ], + "passThroughEnv": [] + }, + "start": { + "cache": false + }, + "test": { + "cache": false + }, + "dev": { + "inputs": ["$TURBO_DEFAULT$", ".env"], + "cache": false, + "persistent": true + }, + "format": {}, + "lint": { + "dependsOn": ["^topo"] + }, + "typecheck": { + "dependsOn": ["^topo"], + "outputs": [] + } + } }