From 257dd73c29cdb016afc20dbd1868d219ac6b888e Mon Sep 17 00:00:00 2001 From: Salazarismo Date: Tue, 17 Mar 2026 17:12:54 -0300 Subject: [PATCH 1/8] feat: align repository with technical guide and fix CI workflow Summary of changes: - Updated documentation (README, ADRs, Changelog) - Implemented runtime contract validation with Zod in NestJS - Migrated to versioned Prisma migrations - Fixed frontend tests and switched to Vitest - Hardened CI workflow with quality gates and native binding fixes --- .github/workflows/ci.yml | 44 +- CHANGELOG.md | 23 + README.md | 63 + apps/api/Dockerfile | 7 +- apps/api/package.json | 22 +- .../20260317000000_init/migration.sql | 29 + .../src/common/pipes/zod-validation.pipe.ts | 24 + .../pos-sync/__tests__/orders.repo.spec.ts | 75 + .../__tests__/pos-sync.controller.spec.ts | 39 + apps/api/src/pos-sync/pos-sync.controller.ts | 6 +- apps/web/package.json | 8 +- .../src/lib/sync/__tests__/enqueue.test.ts | 9 +- apps/web/src/lib/sync/__tests__/retry.test.ts | 15 +- .../web/src/lib/sync/__tests__/runner.test.ts | 162 ++ docs/ADR/001-monorepo-topology.md | 19 + docs/ADR/002-shared-contract-validation.md | 18 + docs/ADR/README.md | 7 + docs/README.md | 172 +- infra/db/init.sql | 12 - infra/docker-compose.yml | 1 - package-lock.json | 1771 ++++++++++++++--- package.json | 4 +- 22 files changed, 2118 insertions(+), 412 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 README.md create mode 100644 apps/api/prisma/migrations/20260317000000_init/migration.sql create mode 100644 apps/api/src/common/pipes/zod-validation.pipe.ts create mode 100644 apps/api/src/pos-sync/__tests__/orders.repo.spec.ts create mode 100644 apps/api/src/pos-sync/__tests__/pos-sync.controller.spec.ts create mode 100644 apps/web/src/lib/sync/__tests__/runner.test.ts create mode 100644 docs/ADR/001-monorepo-topology.md create mode 100644 docs/ADR/002-shared-contract-validation.md create mode 100644 docs/ADR/README.md delete mode 100644 infra/db/init.sql diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b4e26fe..23aa0e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ on: branches: [ main ] jobs: - build-and-test: + quality-gates: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -16,7 +16,6 @@ jobs: with: node-version: '20' cache: 'npm' - cache-dependency-path: package-lock.json - name: Clean and Install dependencies run: | @@ -26,15 +25,38 @@ jobs: - name: Build Shared Contract run: npm run build -w packages/sync-contract - - name: Backend - Lint & Build & Test - run: | - npm run lint -w apps/api - npm run build -w apps/api - npm run test -w apps/api + - name: Typecheck + run: npm run typecheck + + - name: Lint + run: npm run lint + + - name: Backend - Test & Coverage + run: npm run test:cov -w apps/api - - name: Frontend - Lint & Build + - name: Frontend - Test + run: npm run test -w apps/web + + - name: Backend - Prisma Migration Check run: | - npm run lint -w apps/web - npm run build -w apps/web + cd apps/api + npx prisma migrate status + env: + DATABASE_URL: postgres://postgres:postgres@localhost:5432/app # Mock for status check + + - name: Build All + run: npm run build + + release: + needs: quality-gates + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Create Release Draft + uses: softprops/action-gh-release@v2 + with: + draft: true + generate_release_notes: true env: - NEXT_PUBLIC_API_BASE_URL: http://localhost:3001 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..77ce78e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,23 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2026-03-17 + +### Added +- Monorepo topology with NPM Workspaces (`apps/api`, `apps/web`, `packages/sync-contract`). +- Shared Zod schemas for sync contract enforcement. +- Prisma versioned migrations for backend database discipline. +- Structured Winston logging in backend and telemetry signals in frontend. +- Robust CI/CD pipeline with quality gates (Lint, Typecheck, Test Coverage, Migration check). +- Architecture Decision Records (ADRs) for Monorepo and Contract strategy. +- Automated release draft generation on merge to main. + +### Fixed +- Outdated documentation in README and docs/ folder. +- Type-only contract validation replaced with runtime Zod parsing. +- Frontend test runner consistency (switched to Vitest). +- CI native binding issues for Tailwind/LightningCSS. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f8c988e --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# Offline-First POS Monorepo + +[![CI](https://github.com/IndexGrid/offline-first-sync-queue/actions/workflows/ci.yml/badge.svg)](https://github.com/IndexGrid/offline-first-sync-queue/actions/workflows/ci.yml) + +Deterministic, testable, and operationally credible reference implementation of an offline-first Point of Sale (POS) system with batch synchronization. + +## πŸ—οΈ Repository Topology +This project is organized as a monorepo using NPM Workspaces: +- **[apps/api](apps/api)**: NestJS backend service with Prisma and PostgreSQL. +- **[apps/web](apps/web)**: Next.js frontend application with IndexedDB. +- **[packages/sync-contract](packages/sync-contract)**: Shared Zod schemas and TypeScript types (the "Single Source of Truth"). +- **[infra](infra)**: Docker orchestration and database configuration. +- **[docs](docs)**: Architecture Decision Records (ADRs), technical guides, and runbooks. + +## πŸ›‘οΈ Core Invariants +1. **Idempotency**: Client-generated keys (`externalId`) with server-side `ON CONFLICT` enforcement. +2. **State Machine**: Explicit sync transitions (`PENDING` -> `IN_FLIGHT` -> `SYNCED` | `RETRYABLE_ERROR` | `FATAL_ERROR` | `DEAD_LETTER`). +3. **Database Discipline**: Versioned migrations via Prisma (no manual SQL execution in production). +4. **API Hardening**: Versioned endpoints (`v1/pos/sync`) with strict runtime validation. + +## πŸš€ Getting Started + +### Prerequisites +- Node.js >= 20 +- Docker & Docker Compose + +### Quick Start (Docker) +```bash +# Clone and enter +git clone https://github.com/IndexGrid/offline-first-sync-queue.git +cd offline-first-sync-queue + +# Start infrastructure and apps +docker-compose -f infra/docker-compose.yml up -d +``` +Access the dashboard at `http://localhost:3000` and the API at `http://localhost:3001`. + +### Local Development +```bash +# Install all dependencies +npm install + +# Build shared contract first +npm run build -w packages/sync-contract + +# Start services +npm run dev # (If global dev script is configured) +# OR +npm run start:dev -w apps/api +npm run dev -w apps/web +``` + +## πŸ“Š Observability +- **Backend Logs**: Structured Winston logs with request correlation. +- **Frontend Telemetry**: Real-time sync queue depth and status distribution signals. + +## πŸ“„ Documentation +- [Technical Guide](AI-Driven-Implementation-technical-guide.md) +- [Architecture Decision Records (ADRs)](docs/ADR/README.md) +- [Project Documentation](docs/README.md) + +--- +Licensed under MIT. Copyright (c) 2026 Index Grid. diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile index c00eff0..7adcc0e 100644 --- a/apps/api/Dockerfile +++ b/apps/api/Dockerfile @@ -21,9 +21,10 @@ FROM node:20-alpine AS runner WORKDIR /app # Only copy necessary files from builder -COPY --from=builder /app/package*.json ./ -COPY --from=builder /app/dist ./dist -COPY --from=builder /app/node_modules ./node_modules + COPY --from=builder /app/package*.json ./ + COPY --from=builder /app/dist ./dist + COPY --from=builder /app/node_modules ./node_modules + COPY --from=builder /app/prisma ./prisma # Set environment variables ENV NODE_ENV=production diff --git a/apps/api/package.json b/apps/api/package.json index 2559cee..6eaa540 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -14,9 +14,11 @@ "build": "nest build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "start": "nest start", - "start:dev": "nest start --watch", + "start:dev": "npm run prisma:generate && nest start --watch", "start:debug": "nest start --debug --watch", - "start:prod": "node dist/main", + "start:prod": "npm run prisma:migrate && node dist/main", + "prisma:generate": "npx prisma generate", + "prisma:migrate": "npx prisma migrate deploy", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "jest", "test:watch": "jest --watch", @@ -77,9 +79,21 @@ "^.+\\.(t|j)s$": "ts-jest" }, "collectCoverageFrom": [ - "**/*.(t|j)s" + "**/*.(t|j)s", + "!**/main.ts", + "!**/*.module.ts", + "!**/prisma.service.ts", + "!**/common/pipes/*" ], "coverageDirectory": "../coverage", - "testEnvironment": "node" + "testEnvironment": "node", + "coverageThreshold": { + "global": { + "branches": 50, + "functions": 50, + "lines": 50, + "statements": 50 + } + } } } diff --git a/apps/api/prisma/migrations/20260317000000_init/migration.sql b/apps/api/prisma/migrations/20260317000000_init/migration.sql new file mode 100644 index 0000000..fc531c1 --- /dev/null +++ b/apps/api/prisma/migrations/20260317000000_init/migration.sql @@ -0,0 +1,29 @@ +-- CreateTable +CREATE TABLE "orders" ( + "id" TEXT NOT NULL, + "external_id" TEXT NOT NULL, + "entity_type" TEXT NOT NULL DEFAULT 'order', + "payload" JSONB NOT NULL, + "sync_status" TEXT NOT NULL DEFAULT 'PENDING', + "retry_count" INTEGER NOT NULL DEFAULT 0, + "last_error" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "orders_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "orders_external_id_key" ON "orders"("external_id"); + +-- CreateIndex +CREATE INDEX "orders_external_id_idx" ON "orders"("external_id"); + +-- CreateIndex +CREATE INDEX "orders_sync_status_idx" ON "orders"("sync_status"); + +-- CreateIndex +CREATE INDEX "orders_created_at_idx" ON "orders"("created_at"); + +-- CreateIndex +CREATE INDEX "orders_updated_at_idx" ON "orders"("updated_at"); diff --git a/apps/api/src/common/pipes/zod-validation.pipe.ts b/apps/api/src/common/pipes/zod-validation.pipe.ts new file mode 100644 index 0000000..b4195e3 --- /dev/null +++ b/apps/api/src/common/pipes/zod-validation.pipe.ts @@ -0,0 +1,24 @@ +import { + PipeTransform, + Injectable, + ArgumentMetadata, + BadRequestException, +} from '@nestjs/common'; +import type { ZodSchema } from 'zod'; + +@Injectable() +export class ZodValidationPipe implements PipeTransform { + constructor(private schema: ZodSchema) {} + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + transform(value: unknown, metadata: ArgumentMetadata) { + try { + return this.schema.parse(value); + } catch (error) { + throw new BadRequestException( + 'Validation failed: ' + + (error instanceof Error ? error.message : 'Unknown error'), + ); + } + } +} diff --git a/apps/api/src/pos-sync/__tests__/orders.repo.spec.ts b/apps/api/src/pos-sync/__tests__/orders.repo.spec.ts new file mode 100644 index 0000000..0d1313a --- /dev/null +++ b/apps/api/src/pos-sync/__tests__/orders.repo.spec.ts @@ -0,0 +1,75 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { OrdersRepo } from '../orders.repo'; +import { PrismaService } from '../../prisma.service'; + +describe('OrdersRepo', () => { + let repo: OrdersRepo; + let prisma: jest.Mocked; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + OrdersRepo, + { + provide: PrismaService, + useValue: { + order: { + findUnique: jest.fn(), + create: jest.fn(), + update: jest.fn(), + }, + }, + }, + ], + }).compile(); + + repo = module.get(OrdersRepo); + prisma = module.get(PrismaService); + }); + + describe('upsertByExternalId', () => { + const externalId = 'ext-1'; + const payload = { foo: 'bar' }; + + it('should create if not exists', async () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + (prisma.order.findUnique as jest.Mock).mockResolvedValue(null); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + (prisma.order.create as jest.Mock).mockResolvedValue({} as any); + + const result = await repo.upsertByExternalId(externalId, payload); + + expect(result.status).toBe('created'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(prisma.order.create).toHaveBeenCalled(); + }); + + it('should return duplicate if payload is identical', async () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + (prisma.order.findUnique as jest.Mock).mockResolvedValue({ + payload, + } as any); + + const result = await repo.upsertByExternalId(externalId, payload); + + expect(result.status).toBe('duplicate'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(prisma.order.update).not.toHaveBeenCalled(); + }); + + it('should update if payload changed', async () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + (prisma.order.findUnique as jest.Mock).mockResolvedValue({ + payload: { foo: 'old' }, + } as any); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + (prisma.order.update as jest.Mock).mockResolvedValue({} as any); + + const result = await repo.upsertByExternalId(externalId, payload); + + expect(result.status).toBe('updated'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(prisma.order.update).toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/api/src/pos-sync/__tests__/pos-sync.controller.spec.ts b/apps/api/src/pos-sync/__tests__/pos-sync.controller.spec.ts new file mode 100644 index 0000000..cf933af --- /dev/null +++ b/apps/api/src/pos-sync/__tests__/pos-sync.controller.spec.ts @@ -0,0 +1,39 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { PosSyncController } from '../pos-sync.controller'; +import { PosSyncService } from '../pos-sync.service'; +import { SyncBatchRequest } from '@offline-pos/sync-contract'; + +describe('PosSyncController', () => { + let controller: PosSyncController; + let service: jest.Mocked; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [PosSyncController], + providers: [ + { + provide: PosSyncService, + useValue: { + syncBatch: jest.fn(), + }, + }, + ], + }).compile(); + + controller = module.get(PosSyncController); + service = module.get(PosSyncService); + }); + + it('should call service.syncBatch with dto', async () => { + const dto: SyncBatchRequest = { + deviceId: 'dev-1', + items: [], + }; + service.syncBatch.mockResolvedValue({ results: [] }); + + await controller.sync(dto); + + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(service.syncBatch).toHaveBeenCalledWith(dto); + }); +}); diff --git a/apps/api/src/pos-sync/pos-sync.controller.ts b/apps/api/src/pos-sync/pos-sync.controller.ts index 3154e89..67b2a1f 100644 --- a/apps/api/src/pos-sync/pos-sync.controller.ts +++ b/apps/api/src/pos-sync/pos-sync.controller.ts @@ -1,15 +1,19 @@ -import { Body, Controller, Post } from '@nestjs/common'; +import { Body, Controller, Post, UsePipes } from '@nestjs/common'; import { PosSyncService } from './pos-sync.service'; +import { SyncBatchRequestSchema } from '@offline-pos/sync-contract'; import type { SyncBatchRequest, SyncBatchResponse, } from '@offline-pos/sync-contract'; +import { ZodValidationPipe } from '../common/pipes/zod-validation.pipe'; @Controller('v1/pos') export class PosSyncController { constructor(private readonly svc: PosSyncService) {} @Post('sync') + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + @UsePipes(new ZodValidationPipe(SyncBatchRequestSchema as any)) async sync(@Body() dto: SyncBatchRequest): Promise { return this.svc.syncBatch(dto); } diff --git a/apps/web/package.json b/apps/web/package.json index 37d7c24..053f7d4 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -15,8 +15,8 @@ "build": "next build", "start": "next start", "lint": "eslint src", - "test": "jest", - "test:watch": "jest --watch" + "test": "vitest run", + "test:watch": "vitest" }, "dependencies": { "@offline-pos/sync-contract": "*", @@ -47,6 +47,8 @@ "eslint": "^9", "eslint-config-next": "16.1.6", "tailwindcss": "^4", - "typescript": "^5" + "typescript": "^5", + "vitest": "^2.1.8", + "fake-indexeddb": "^6.0.0" } } diff --git a/apps/web/src/lib/sync/__tests__/enqueue.test.ts b/apps/web/src/lib/sync/__tests__/enqueue.test.ts index 756eb76..556e618 100644 --- a/apps/web/src/lib/sync/__tests__/enqueue.test.ts +++ b/apps/web/src/lib/sync/__tests__/enqueue.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { getDB } from '../db'; +import 'fake-indexeddb/auto'; +import { getDB } from '../../db'; import { enqueueOrder } from '../enqueue'; describe('enqueueOrder', () => { @@ -40,8 +41,10 @@ describe('enqueueOrder', () => { // Check order was created const order = await db.get('orders', result.externalId); expect(order).toBeDefined(); - expect(order.data).toEqual(orderData); - expect(order.syncStatus).toBe('LOCAL_ONLY'); + if (order) { + expect(order.data).toEqual(orderData); + expect(order.syncStatus).toBe('LOCAL_ONLY'); + } // Check sync queue item was created const syncItems = await db.getAll('syncQueue'); diff --git a/apps/web/src/lib/sync/__tests__/retry.test.ts b/apps/web/src/lib/sync/__tests__/retry.test.ts index e29a104..7a05693 100644 --- a/apps/web/src/lib/sync/__tests__/retry.test.ts +++ b/apps/web/src/lib/sync/__tests__/retry.test.ts @@ -1,14 +1,21 @@ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; +import 'fake-indexeddb/auto'; import { computeNextAttemptAt, shouldRetry } from '../retry'; describe('computeNextAttemptAt', () => { it('should increase delay with retry count', () => { - const attempt1 = computeNextAttemptAt(0); - const attempt2 = computeNextAttemptAt(1); - const attempt3 = computeNextAttemptAt(2); + vi.spyOn(Math, 'random').mockReturnValue(1.0); // Use max jitter for test + const now = 1000000; + vi.spyOn(Date, 'now').mockReturnValue(now); + + const attempt1 = computeNextAttemptAt(0); // 1000000 + 1.0 * 1000 = 1001000 + const attempt2 = computeNextAttemptAt(1); // 1000000 + 1.0 * 2000 = 1002000 + const attempt3 = computeNextAttemptAt(2); // 1000000 + 1.0 * 4000 = 1004000 expect(attempt2).toBeGreaterThan(attempt1); expect(attempt3).toBeGreaterThan(attempt2); + + vi.restoreAllMocks(); }); it('should cap at 60 seconds', () => { diff --git a/apps/web/src/lib/sync/__tests__/runner.test.ts b/apps/web/src/lib/sync/__tests__/runner.test.ts new file mode 100644 index 0000000..23ced91 --- /dev/null +++ b/apps/web/src/lib/sync/__tests__/runner.test.ts @@ -0,0 +1,162 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import 'fake-indexeddb/auto'; +import { runSyncOnce } from '../runner'; +import { getDB } from '../../db'; + +describe('runSyncOnce', () => { + beforeEach(async () => { + // Clear the DB before each test + const db = await getDB(); + const tx = db.transaction(['syncQueue', 'orders'], 'readwrite'); + await tx.objectStore('syncQueue').clear(); + await tx.objectStore('orders').clear(); + await tx.done; + + vi.clearAllMocks(); + }); + + it('should group items by URL and send batches', async () => { + const db = await getDB(); + const now = Date.now(); + + // Add items to the queue + const tx = db.transaction('syncQueue', 'readwrite'); + await tx.objectStore('syncQueue').add({ + id: '1', + externalId: 'ext-1', + entityType: 'order', + status: 'PENDING', + nextAttemptAt: now - 1000, + createdAt: now - 2000, + payload: { data: 'test1' }, + url: 'v1/pos/sync', + method: 'POST', + op: 'UPSERT', + retryCount: 0 + }); + await tx.done; + + const mockFetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ + results: [{ externalId: 'ext-1', status: 'created' }] + }) + }); + + const result = await runSyncOnce({ + batchSize: 10, + fetchImpl: mockFetch as unknown as typeof fetch, + deviceId: 'test-device' + }); + + expect(result.sent).toBe(1); + expect(result.acked).toBe(1); + expect(mockFetch).toHaveBeenCalled(); + + // Check if item status is updated to SYNCED + const updatedItem = await db.get('syncQueue', '1'); + expect(updatedItem?.status).toBe('SYNCED'); + }); + + it('should handle partial failures in a batch', async () => { + const db = await getDB(); + const now = Date.now(); + + const tx = db.transaction('syncQueue', 'readwrite'); + await tx.objectStore('syncQueue').add({ + id: '1', + externalId: 'ext-1', + entityType: 'order', + status: 'PENDING', + nextAttemptAt: now - 1000, + createdAt: now - 2000, + payload: { data: 'test1' }, + url: 'v1/pos/sync', + method: 'POST', + op: 'UPSERT', + retryCount: 0 + }); + await tx.objectStore('syncQueue').add({ + id: '2', + externalId: 'ext-2', + entityType: 'order', + status: 'PENDING', + nextAttemptAt: now - 1000, + createdAt: now - 2000, + payload: { data: 'test2' }, + url: 'v1/pos/sync', + method: 'POST', + op: 'UPSERT', + retryCount: 0 + }); + await tx.done; + + const mockFetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ + results: [ + { externalId: 'ext-1', status: 'created' }, + { externalId: 'ext-2', status: 'error', reason: 'validation_failed' } + ] + }) + }); + + const result = await runSyncOnce({ + batchSize: 10, + fetchImpl: mockFetch as unknown as typeof fetch, + maxRetries: 3 + }); + + expect(result.acked).toBe(1); + expect(result.dead).toBe(0); // Should be retryable error initially + + const item1 = await db.get('syncQueue', '1'); + const item2 = await db.get('syncQueue', '2'); + + expect(item1?.status).toBe('SYNCED'); + expect(item2?.status).toBe('RETRYABLE_ERROR'); + expect(item2?.retryCount).toBe(1); + }); + + it('should recover stale IN_FLIGHT items', async () => { + const db = await getDB(); + const now = Date.now(); + + const tx = db.transaction('syncQueue', 'readwrite'); + await tx.objectStore('syncQueue').add({ + id: '1', + externalId: 'ext-1', + entityType: 'order', + status: 'IN_FLIGHT', + inFlightAt: now - 120000, // 2 minutes ago + nextAttemptAt: now - 130000, + createdAt: now - 140000, + payload: { data: 'stale' }, + url: 'v1/pos/sync', + method: 'POST', + op: 'UPSERT', + retryCount: 0 + }); + await tx.done; + + const mockFetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ + results: [{ externalId: 'ext-1', status: 'created' }] + }) + }); + + // Run sync, it should first recover the stale item + const result = await runSyncOnce({ + batchSize: 10, + fetchImpl: mockFetch as unknown as typeof fetch, + staleInFlightAfterMs: 60000 // 1 minute + }); + + expect(result.sent).toBe(1); + expect(result.acked).toBe(1); + + const recoveredItem = await db.get('syncQueue', '1'); + expect(recoveredItem?.status).toBe('SYNCED'); + }); +}); diff --git a/docs/ADR/001-monorepo-topology.md b/docs/ADR/001-monorepo-topology.md new file mode 100644 index 0000000..52fba15 --- /dev/null +++ b/docs/ADR/001-monorepo-topology.md @@ -0,0 +1,19 @@ +# ADR 001: Monorepo Topology + +## Status +Accepted + +## Context +The project requires a tightly coupled contract between the frontend (client) and the backend (server) to ensure reliable offline synchronization. Managing separate repositories for these components increases the risk of contract drift and overhead in dependency management. + +## Decision +We will use a Monorepo topology using **NPM Workspaces**. + +### Structure: +- `apps/api`: NestJS backend. +- `apps/web`: Next.js frontend. +- `packages/sync-contract`: Shared validation schemas (Zod) and types. + +## Consequences +- **Pros**: Single source of truth for contracts, simplified CI/CD orchestration, atomic changes across client/server. +- **Cons**: Requires explicit build order (contract first), slightly larger initial clone size. diff --git a/docs/ADR/002-shared-contract-validation.md b/docs/ADR/002-shared-contract-validation.md new file mode 100644 index 0000000..2f6caf4 --- /dev/null +++ b/docs/ADR/002-shared-contract-validation.md @@ -0,0 +1,18 @@ +# ADR 002: Shared Contract Validation + +## Status +Accepted + +## Context +The synchronization between client and server must be strictly validated at runtime to prevent data corruption or inconsistencies in an offline-first environment. Type-only validation is insufficient. + +## Decision +We will use **Zod** as the primary validation engine for the shared sync contract (`packages/sync-contract`). + +### Enforcement: +- **Frontend**: Used in `IndexedDB` schemas and before transmission in the `runner`. +- **Backend**: Used in `PosSyncController` as the runtime validation gate at the transport boundary (via `ValidationPipe` or direct Zod parsing). + +## Consequences +- **Pros**: Strong runtime guarantees, single schema definition shared by both ends, type-safety derived from schema. +- **Cons**: Performance overhead for parsing (negligible for our use case). diff --git a/docs/ADR/README.md b/docs/ADR/README.md new file mode 100644 index 0000000..ade879b --- /dev/null +++ b/docs/ADR/README.md @@ -0,0 +1,7 @@ +# Architectural Decision Records (ADRs) + +This directory contains records of significant architectural decisions made in the project. + +## Index +- [ADR 001: Monorepo Topology](001-monorepo-topology.md) +- [ADR 002: Shared Contract Validation](002-shared-contract-validation.md) diff --git a/docs/README.md b/docs/README.md index f917334..f8cefe1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,174 +1,72 @@ -# Offline-First POS System with Batch Sync +# Project Documentation -[![CI](https://github.com/IndexGrid/offline-first-sync-queue/actions/workflows/ci.yml/badge.svg)](https://github.com/IndexGrid/offline-first-sync-queue/actions/workflows/ci.yml) +This directory contains the detailed documentation for the Offline-First POS System. -This project implements a Point of Sale (POS) system with **offline-first** support and automatic batch synchronization. The system allows creating orders even without an internet connection and automatically synchronizes once the connection is restored. - -## πŸ“ Repository Structure +## πŸ“ Monorepo Structure ```text . -β”œβ”€β”€ frontend/ # Next.js web application -β”‚ β”œβ”€β”€ src/ -β”‚ β”‚ β”œβ”€β”€ components/ # UI Components (OrderForm, SyncDashboard, etc.) -β”‚ β”‚ β”œβ”€β”€ lib/ -β”‚ β”‚ β”‚ β”œβ”€β”€ db.ts # IndexedDB schema and configuration -β”‚ β”‚ β”‚ └── sync/ # Sync engine (runner, enqueue, retry, lock) -β”‚ β”‚ └── app/ # Next.js App Router pages -β”‚ └── .env.example # Frontend environment template -β”œβ”€β”€ frontend/backend/ # NestJS API application -β”‚ β”œβ”€β”€ src/ -β”‚ β”‚ β”œβ”€β”€ pos-sync/ # Core logic (Controller, Service, Repo) -β”‚ β”‚ └── main.ts # Entry point -β”‚ └── .env.example # Backend environment template -β”œβ”€β”€ docker-compose.yml # Infrastructure orchestration -β”œβ”€β”€ init.sql # PostgreSQL initial schema -└── README.md # Documentation +β”œβ”€β”€ apps/ +β”‚ β”œβ”€β”€ api/ # NestJS API (PostgreSQL + Prisma) +β”‚ └── web/ # Next.js Frontend (IndexedDB) +β”œβ”€β”€ packages/ +β”‚ └── sync-contract/ # Shared Zod schemas & TS types +β”œβ”€β”€ infra/ +β”‚ β”œβ”€β”€ db/ # DB init scripts (for local dev) +β”‚ └── docker-compose.yml # Orchestration +└── docs/ + β”œβ”€β”€ ADR/ # Architectural Decision Records + └── README.md # You are here ``` -## πŸš€ Features - -### Frontend (Next.js + IndexedDB) -- βœ… **Offline Order Creation** - Works without an internet connection. -- βœ… **Sync Queue** - Intelligent local queue management with states. -- βœ… **Batch Sync** - Optimized batch sending with automatic retry. -- βœ… **Monitoring Dashboard** - Real-time visualization of sync status. -- βœ… **Local Deduplication** - Prevents duplicate orders from double-clicks. -- βœ… **Robust Error Handling** - Retry with exponential backoff and jitter. - -### Backend (NestJS + PostgreSQL) -- βœ… **Idempotent REST API** - Secure processing with unique `externalId`. -- βœ… **Per-item Validation** - Each item is validated individually without affecting the whole batch. -- βœ… **Smart Upsert** - Detects creation, update, or duplication. -- βœ… **PostgreSQL Integration** - Persistent storage with optimized indexes. - -## πŸ“‘ API Contract +## πŸ“‘ Sync API Contract (v1) ### Batch Synchronization -`POST /admin/pos/sync` +`POST v1/pos/sync` **Request Body:** ```json { "deviceId": "pos-001", - "orders": [ + "items": [ { "externalId": "550e8400-e29b-41d4-a716-446655440000", - "data": { + "entityType": "order", + "payload": { "items": [{"sku": "PROD001", "qty": 2, "price": 10.00}], "total": 20.00, "customer": "John Doe" } - }, - { - "externalId": "invalid-uuid", - "data": { "items": [], "total": 0 } } ] } ``` -**Response Body (201 Created):** +**Response Body:** ```json { "results": [ { "externalId": "550e8400-e29b-41d4-a716-446655440000", "status": "created" - }, - { - "externalId": "invalid-uuid", - "status": "invalid", - "reason": "externalId must be a UUID" } ] } ``` *Possible statuses: `created`, `updated`, `duplicate`, `invalid`, `auth_required`, `error`.* -## πŸ“‹ Workflow - -### 1. Offline Order Creation -1. User fills out the order form. -2. System generates a unique `externalId` (UUID v4) on the client. -3. Order is saved in IndexedDB with `LOCAL_ONLY` status. -4. A synchronization event is created in the queue with `PENDING` status. -5. Immediate confirmation is shown to the user. - -### 2. Batch Synchronization -1. Runner detects an online connection or interval (15s). -2. Collects up to 50 `PENDING` items from the local queue. -3. Groups by endpoint and splits into chunks (max 256KB). -4. Sends the batch to the backend with optional gzip compression. -5. Processes the response item by item, updating local states. -6. Marks orders as `SYNCED` or `ERROR` based on the response. - -## πŸ› οΈ Installation and Execution - -### Option 1: Docker Compose (Recommended) - -```bash -# Clone the repository -git clone https://github.com/IndexGrid/offline-first-sync-queue.git -cd offline-first-sync-queue - -# Start all services -docker-compose up -d --build -``` - -### Option 2: Local Development - -1. **Setup Backend**: - ```bash - cd frontend/backend - cp .env.example .env - npm install - npm run start:dev - ``` -2. **Setup Frontend**: - ```bash - cd frontend - cp .env.example .env.local - npm install - npm run dev - ``` - -## πŸ§ͺ Testing Scenarios - -### 1. Happy Path (Online) -- Create an order at `http://localhost:3000`. -- Observe the order list: status should transition from `LOCAL_ONLY` to `SYNCED` within seconds. -- Verify in database: `docker exec -it postgres psql -U postgres -d app -c "SELECT * FROM orders;"` - -### 2. Offline Resilience -- Turn off your internet or set Chrome DevTools to **Offline**. -- Create 3 orders. They will remain as `LOCAL_ONLY`. -- Check `http://localhost:3000/sync/status` to see 3 items in `PENDING` state. -- Restore connection. Observe automatic synchronization. - -### 3. Idempotency & Conflict -- The system prevents duplicates even if the same request is sent twice (e.g., network retry). -- The backend uses `ON CONFLICT (external_id) DO UPDATE` to ensure consistency. - -## 🧠 Design Decisions & Trade-offs - -| Decision | Rationale | -| :--- | :--- | -| **IndexedDB vs localStorage** | IndexedDB is asynchronous, supports larger data volumes, and allows complex indexing, which is essential for a sync queue. | -| **Batch size (50)** | Balances request overhead and payload size. Prevents timeouts while keeping throughput high. | -| **Payload limit (256KB)** | Avoids hitting default server body-parser limits and ensures reliable transmission on weak connections. | -| **Dedupe best-effort** | Uses FNV-1a hashing on the client with a 2-second window to prevent UI-level accidental double-submissions. | -| **Single-tab locking** | To prevent race conditions where multiple tabs try to process the same sync queue simultaneously. | - -## 🚨 Known Limitations - -- **No CRDT/merge**: Eventual consistency only. Last-write-wins at the record level. -- **No Service Worker**: Synchronization only happens while the app is open in a tab. - -## 🀝 Contributing - -Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. - -## πŸ“„ License - -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. +## πŸ”„ Synchronization Workflow + +1. **Local Creation**: Data is saved to IndexedDB with `LOCAL_ONLY` status. +2. **Queueing**: A sync event is added to the `syncQueue` as `PENDING`. +3. **Runner**: The background runner collects `PENDING` items, groups them by endpoint, and marks them `IN_FLIGHT`. +4. **Transport**: Batches are sent via `POST v1/pos/sync` (optionally gzipped). +5. **Reconciliation**: + - `created`/`updated`/`duplicate` -> Local state becomes `SYNCED`. + - `invalid`/`error` -> Local state transitions to `FATAL_ERROR` or `RETRYABLE_ERROR`. +6. **Recovery**: Items stuck in `IN_FLIGHT` for too long are automatically requeued. + +## πŸ› οΈ Operational Guides +- [Architecture Decision Records (ADRs)](ADR/README.md) +- [Local Development Guide](../README.md#local-development) +- [CI/CD Pipeline](../.github/workflows/ci.yml) diff --git a/infra/db/init.sql b/infra/db/init.sql deleted file mode 100644 index 7e85294..0000000 --- a/infra/db/init.sql +++ /dev/null @@ -1,12 +0,0 @@ --- create table -CREATE TABLE orders ( - id BIGSERIAL PRIMARY KEY, - external_id UUID NOT NULL UNIQUE, - payload JSONB NOT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now() -); - --- create index -CREATE INDEX idx_orders_external_id ON orders(external_id); -CREATE INDEX idx_orders_created_at ON orders(created_at); \ No newline at end of file diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml index a49e268..9f81328 100644 --- a/infra/docker-compose.yml +++ b/infra/docker-compose.yml @@ -9,7 +9,6 @@ services: - "5432:5432" volumes: - postgres_data:/var/lib/postgresql/data - - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql api: build: ../apps/api diff --git a/package-lock.json b/package-lock.json index 86021f8..34c0292 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1771,11 +1771,6 @@ "@jridgewell/trace-mapping": "^0.3.25" } }, - "apps/api/node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "dev": true, - "license": "MIT" - }, "apps/api/node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "dev": true, @@ -2404,11 +2399,6 @@ "@types/estree": "*" } }, - "apps/api/node_modules/@types/estree": { - "version": "1.0.8", - "dev": true, - "license": "MIT" - }, "apps/api/node_modules/@types/express": { "version": "5.0.6", "dev": true, @@ -3875,21 +3865,6 @@ "node": ">= 8" } }, - "apps/api/node_modules/debug": { - "version": "4.4.3", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "apps/api/node_modules/dedent": { "version": "1.7.2", "dev": true, @@ -6291,10 +6266,6 @@ "node": ">=16 || 14 >=14.17" } }, - "apps/api/node_modules/ms": { - "version": "2.1.3", - "license": "MIT" - }, "apps/api/node_modules/multer": { "version": "2.1.1", "license": "MIT", @@ -6727,11 +6698,6 @@ "split2": "^4.1.0" } }, - "apps/api/node_modules/picocolors": { - "version": "1.1.1", - "dev": true, - "license": "ISC" - }, "apps/api/node_modules/picomatch": { "version": "4.0.2", "dev": true, @@ -8548,8 +8514,10 @@ "babel-plugin-react-compiler": "1.0.0", "eslint": "^9", "eslint-config-next": "16.1.6", + "fake-indexeddb": "^6.0.0", "tailwindcss": "^4", - "typescript": "^5" + "typescript": "^5", + "vitest": "^2.1.8" }, "engines": { "node": ">=20" @@ -9005,11 +8973,6 @@ "node": ">=6.0.0" } }, - "apps/web/node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "dev": true, - "license": "MIT" - }, "apps/web/node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "dev": true, @@ -9131,11 +9094,6 @@ "tailwindcss": "4.2.1" } }, - "apps/web/node_modules/@types/estree": { - "version": "1.0.8", - "dev": true, - "license": "MIT" - }, "apps/web/node_modules/@types/json-schema": { "version": "7.0.15", "dev": true, @@ -9959,22 +9917,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "apps/web/node_modules/debug": { - "version": "4.4.3", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "apps/web/node_modules/deep-is": { "version": "0.1.4", "dev": true, @@ -11672,14 +11614,6 @@ "yallist": "^3.0.2" } }, - "apps/web/node_modules/magic-string": { - "version": "0.30.21", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, "apps/web/node_modules/math-intrinsics": { "version": "1.1.0", "dev": true, @@ -11727,27 +11661,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "apps/web/node_modules/ms": { - "version": "2.1.3", - "dev": true, - "license": "MIT" - }, - "apps/web/node_modules/nanoid": { - "version": "3.3.11", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, "apps/web/node_modules/napi-postinstall": { "version": "0.3.4", "dev": true, @@ -12065,10 +11978,6 @@ "dev": true, "license": "MIT" }, - "apps/web/node_modules/picocolors": { - "version": "1.1.1", - "license": "ISC" - }, "apps/web/node_modules/picomatch": { "version": "2.3.1", "dev": true, @@ -12088,33 +11997,6 @@ "node": ">= 0.4" } }, - "apps/web/node_modules/postcss": { - "version": "8.5.8", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, "apps/web/node_modules/prelude-ls": { "version": "1.2.1", "dev": true, @@ -12535,13 +12417,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "apps/web/node_modules/source-map-js": { - "version": "1.2.1", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "apps/web/node_modules/stable-hash": { "version": "0.0.5", "dev": true, @@ -13175,150 +13050,898 @@ "zod": "^3.25.0 || ^4.0.0" } }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz", - "integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", "cpu": [ - "x64" + "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "aix" ], "engines": { - "node": ">= 10" + "node": ">=12" } }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz", - "integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==", + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "cpu": [ - "x64" + "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "android" ], "engines": { - "node": ">= 10" + "node": ">=12" } }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz", - "integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==", + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "android" ], "engines": { - "node": ">= 10" + "node": ">=12" } }, - "node_modules/@offline-pos/api": { - "resolved": "apps/api", - "link": true + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@offline-pos/sync-contract": { - "resolved": "packages/sync-contract", - "link": true + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@offline-pos/web": { - "resolved": "apps/web", - "link": true + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@prisma/client": { - "version": "6.19.2", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.2.tgz", - "integrity": "sha512-gR2EMvfK/aTxsuooaDA32D8v+us/8AAet+C3J1cc04SW35FPdZYgLF+iN4NDLUgAaUGTKdAB0CYenu1TAgGdMg==", - "hasInstallScript": true, - "license": "Apache-2.0", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=18.18" - }, - "peerDependencies": { - "prisma": "*", - "typescript": ">=5.1.0" - }, - "peerDependenciesMeta": { - "prisma": { - "optional": true - }, - "typescript": { - "optional": true - } + "node": ">=12" } }, - "node_modules/@prisma/config": { - "version": "6.19.2", - "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.2.tgz", - "integrity": "sha512-kadBGDl+aUswv/zZMk9Mx0C8UZs1kjao8H9/JpI4Wh4SHZaM7zkTwiKn/iFLfRg+XtOAo/Z/c6pAYhijKl0nzQ==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "c12": "3.1.0", - "deepmerge-ts": "7.1.5", - "effect": "3.18.4", - "empathic": "2.0.0" + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@prisma/debug": { - "version": "6.19.2", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.2.tgz", - "integrity": "sha512-lFnEZsLdFLmEVCVNdskLDCL8Uup41GDfU0LUfquw+ercJC8ODTuL0WNKgOKmYxCJVvFwf0OuZBzW99DuWmoH2A==", - "devOptional": true, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz", + "integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz", + "integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz", + "integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@offline-pos/api": { + "resolved": "apps/api", + "link": true + }, + "node_modules/@offline-pos/sync-contract": { + "resolved": "packages/sync-contract", + "link": true + }, + "node_modules/@offline-pos/web": { + "resolved": "apps/web", + "link": true + }, + "node_modules/@prisma/client": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.2.tgz", + "integrity": "sha512-gR2EMvfK/aTxsuooaDA32D8v+us/8AAet+C3J1cc04SW35FPdZYgLF+iN4NDLUgAaUGTKdAB0CYenu1TAgGdMg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.2.tgz", + "integrity": "sha512-kadBGDl+aUswv/zZMk9Mx0C8UZs1kjao8H9/JpI4Wh4SHZaM7zkTwiKn/iFLfRg+XtOAo/Z/c6pAYhijKl0nzQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.18.4", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.2.tgz", + "integrity": "sha512-lFnEZsLdFLmEVCVNdskLDCL8Uup41GDfU0LUfquw+ercJC8ODTuL0WNKgOKmYxCJVvFwf0OuZBzW99DuWmoH2A==", + "devOptional": true, "license": "Apache-2.0" }, - "node_modules/@prisma/engines": { - "version": "6.19.2", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.2.tgz", - "integrity": "sha512-TTkJ8r+uk/uqczX40wb+ODG0E0icVsMgwCTyTHXehaEfb0uo80M9g1aW1tEJrxmFHeOZFXdI2sTA1j1AgcHi4A==", - "devOptional": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "6.19.2", - "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", - "@prisma/fetch-engine": "6.19.2", - "@prisma/get-platform": "6.19.2" - } + "node_modules/@prisma/engines": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.2.tgz", + "integrity": "sha512-TTkJ8r+uk/uqczX40wb+ODG0E0icVsMgwCTyTHXehaEfb0uo80M9g1aW1tEJrxmFHeOZFXdI2sTA1j1AgcHi4A==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.2", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/fetch-engine": "6.19.2", + "@prisma/get-platform": "6.19.2" + } + }, + "node_modules/@prisma/engines-version": { + "version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz", + "integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.2.tgz", + "integrity": "sha512-h4Ff4Pho+SR1S8XerMCC12X//oY2bG3Iug/fUnudfcXEUnIeRiBdXHFdGlGOgQ3HqKgosTEhkZMvGM9tWtYC+Q==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.2", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/get-platform": "6.19.2" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.2.tgz", + "integrity": "sha512-PGLr06JUSTqIvztJtAzIxOwtWKtJm5WwOG6xpsgD37Rc84FpfUBGLKz65YpJBGtkRQGXTYEFie7pYALocC3MtA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.2" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@prisma/engines-version": { - "version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz", - "integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==", - "devOptional": true, - "license": "Apache-2.0" + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@prisma/fetch-engine": { - "version": "6.19.2", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.2.tgz", - "integrity": "sha512-h4Ff4Pho+SR1S8XerMCC12X//oY2bG3Iug/fUnudfcXEUnIeRiBdXHFdGlGOgQ3HqKgosTEhkZMvGM9tWtYC+Q==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "6.19.2", - "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", - "@prisma/get-platform": "6.19.2" - } + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@prisma/get-platform": { - "version": "6.19.2", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.2.tgz", - "integrity": "sha512-PGLr06JUSTqIvztJtAzIxOwtWKtJm5WwOG6xpsgD37Rc84FpfUBGLKz65YpJBGtkRQGXTYEFie7pYALocC3MtA==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "6.19.2" - } + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, "node_modules/@standard-schema/spec": { "version": "1.1.0", @@ -13375,6 +13998,150 @@ "node": ">= 20" } }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/c12": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", @@ -13404,6 +14171,43 @@ } } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -13443,7 +14247,34 @@ "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", "license": "MIT", "engines": { - "node": "^14.18.0 || >=16.10.0" + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" } }, "node_modules/deepmerge-ts": { @@ -13504,6 +14335,72 @@ "node": ">=14" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/exsolve": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", @@ -13511,6 +14408,16 @@ "devOptional": true, "license": "MIT" }, + "node_modules/fake-indexeddb": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/fake-indexeddb/-/fake-indexeddb-6.2.5.tgz", + "integrity": "sha512-CGnyrvbhPlWYMngksqrSSUT1BAVP49dZocrHuK0SvtR0D5TMs5wP0o3j7jexDJW01KSadjBp1M/71o/KR3nD1w==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, "node_modules/fast-check": { "version": "3.23.2", "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", @@ -13534,6 +14441,21 @@ "node": ">=8.0.0" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/giget": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", @@ -13782,6 +14704,47 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/node-fetch-native": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", @@ -13828,6 +14791,16 @@ "devOptional": true, "license": "MIT" }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/perfect-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", @@ -13835,6 +14808,12 @@ "devOptional": true, "license": "MIT" }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, "node_modules/pkg-types": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", @@ -13847,6 +14826,35 @@ "pathe": "^2.0.3" } }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/prisma": { "version": "6.19.2", "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.2.tgz", @@ -13916,6 +14924,88 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyexec": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", @@ -13926,6 +15016,36 @@ "node": ">=18" } }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -13941,6 +15061,193 @@ "node": ">=14.17" } }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vitest/node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/package.json b/package.json index 252459d..48c7e42 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,8 @@ "scripts": { "build": "npm run build -w packages/sync-contract && npm run build -w apps/api && npm run build -w apps/web", "test": "npm run test -w apps/api && npm run test -w apps/web", - "lint": "npm run lint -w apps/api && npm run lint -w apps/web" + "test:cov": "npm run test:cov -w apps/api && npm run test -w apps/web", + "lint": "npm run lint -w apps/api && npm run lint -w apps/web", + "typecheck": "npm run build -w packages/sync-contract && tsc --project apps/api/tsconfig.json --noEmit && tsc --project apps/web/tsconfig.json --noEmit" } } From b4027adfccfd21152bb02b674c4633ebc59f1742 Mon Sep 17 00:00:00 2001 From: Salazarismo Date: Tue, 17 Mar 2026 17:17:39 -0300 Subject: [PATCH 2/8] fix: resolve unsafe-return lint error in ZodValidationPipe --- apps/api/src/common/pipes/zod-validation.pipe.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/src/common/pipes/zod-validation.pipe.ts b/apps/api/src/common/pipes/zod-validation.pipe.ts index b4195e3..1047091 100644 --- a/apps/api/src/common/pipes/zod-validation.pipe.ts +++ b/apps/api/src/common/pipes/zod-validation.pipe.ts @@ -11,7 +11,7 @@ export class ZodValidationPipe implements PipeTransform { constructor(private schema: ZodSchema) {} // eslint-disable-next-line @typescript-eslint/no-unused-vars - transform(value: unknown, metadata: ArgumentMetadata) { + transform(value: unknown, metadata: ArgumentMetadata): unknown { try { return this.schema.parse(value); } catch (error) { From 74316a0838f033acbcab7afb8bf09f3cedeedef1 Mon Sep 17 00:00:00 2001 From: Salazarismo Date: Tue, 17 Mar 2026 17:21:37 -0300 Subject: [PATCH 3/8] fix: replace prisma migrate status with prisma validate in CI --- .github/workflows/ci.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 23aa0e4..0092537 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,12 +37,10 @@ jobs: - name: Frontend - Test run: npm run test -w apps/web - - name: Backend - Prisma Migration Check + - name: Backend - Prisma Schema Validation run: | cd apps/api - npx prisma migrate status - env: - DATABASE_URL: postgres://postgres:postgres@localhost:5432/app # Mock for status check + npx prisma validate - name: Build All run: npm run build From 71e323262006e81fb9569ae879a2df798433e8ad Mon Sep 17 00:00:00 2001 From: Salazarismo Date: Tue, 17 Mar 2026 17:24:33 -0300 Subject: [PATCH 4/8] fix: provide dummy DATABASE_URL for prisma validate in CI --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0092537..07dc030 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,6 +41,8 @@ jobs: run: | cd apps/api npx prisma validate + env: + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/dummy - name: Build All run: npm run build From 85b96c0ae01fa72d1b396f79e37504396abe96c5 Mon Sep 17 00:00:00 2001 From: Salazarismo Date: Tue, 17 Mar 2026 17:39:13 -0300 Subject: [PATCH 5/8] docs: finalize alignment with technical guide and expand test matrix Summary of changes: - Created docs/RUNBOOK.md for operational guidance. - Rewrote apps/api and apps/web READMEs to remove boilerplate. - Expanded backend tests to cover duplicate, invalid, and auth_required scenarios. - Expanded frontend integration tests to cover DEAD_LETTER and network backoff logic. - Finalized doc alignment in docs/README.md by removing dead infra/db references. --- apps/api/README.md | 109 +++--------------- .../__tests__/pos-sync.service.spec.ts | 66 +++++++++++ apps/web/README.md | 46 +++----- .../web/src/lib/sync/__tests__/runner.test.ts | 73 ++++++++++++ docs/README.md | 2 +- docs/RUNBOOK.md | 47 ++++++++ 6 files changed, 216 insertions(+), 127 deletions(-) create mode 100644 docs/RUNBOOK.md diff --git a/apps/api/README.md b/apps/api/README.md index 8f0f65f..a4186d0 100644 --- a/apps/api/README.md +++ b/apps/api/README.md @@ -1,98 +1,19 @@ -

- Nest Logo -

+# @offline-pos/api -[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 -[circleci-url]: https://circleci.com/gh/nestjs/nest +NestJS service responsible for the server-side logic of the Offline-First POS system. -

A progressive Node.js framework for building efficient and scalable server-side applications.

-

-NPM Version -Package License -NPM Downloads -CircleCI -Discord -Backers on Open Collective -Sponsors on Open Collective - Donate us - Support us - Follow us on Twitter -

- +## πŸ“‘ Responsibilities +- **Idempotent Sync**: Processes batch requests from POS devices via `POST v1/pos/sync`. +- **Contract Enforcement**: Strict runtime validation using Zod schemas from `@offline-pos/sync-contract`. +- **Persistence**: Managed via Prisma ORM with PostgreSQL. -## Description +## πŸ› οΈ Key Scripts +- `npm run prisma:migrate`: Deploys database migrations. +- `npm run test:cov`: Runs unit tests with coverage reporting. +- `npm run lint`: Enforces code style and best practices. -[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. - -## Project setup - -```bash -$ npm install -``` - -## Compile and run the project - -```bash -# development -$ npm run start - -# watch mode -$ npm run start:dev - -# production mode -$ npm run start:prod -``` - -## Run tests - -```bash -# unit tests -$ npm run test - -# e2e tests -$ npm run test:e2e - -# test coverage -$ npm run test:cov -``` - -## Deployment - -When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information. - -If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps: - -```bash -$ npm install -g @nestjs/mau -$ mau deploy -``` - -With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure. - -## Resources - -Check out a few resources that may come in handy when working with NestJS: - -- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework. -- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy). -- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/). -- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks. -- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com). -- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com). -- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs). -- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com). - -## Support - -Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). - -## Stay in touch - -- Author - [Kamil MyΕ›liwiec](https://twitter.com/kammysliwiec) -- Website - [https://nestjs.com](https://nestjs.com/) -- Twitter - [@nestframework](https://twitter.com/nestframework) - -## License - -Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE). +## πŸ—οΈ Architecture +- **Controllers**: Entry point for HTTP requests. +- **Services**: Business logic (sync reconciliation, idempotency). +- **Repositories**: Data access layer via Prisma. +- **Pipes**: Global `ZodValidationPipe` for contract hardening. diff --git a/apps/api/src/pos-sync/__tests__/pos-sync.service.spec.ts b/apps/api/src/pos-sync/__tests__/pos-sync.service.spec.ts index 437dd2d..3e9fcbe 100644 --- a/apps/api/src/pos-sync/__tests__/pos-sync.service.spec.ts +++ b/apps/api/src/pos-sync/__tests__/pos-sync.service.spec.ts @@ -114,5 +114,71 @@ describe('PosSyncService', () => { // eslint-disable-next-line @typescript-eslint/unbound-method expect(ordersRepo.upsertByExternalId).toHaveBeenCalledTimes(2); }); + + it('should handle duplicate items', async () => { + const validUuid = '550e8400-e29b-41d4-a716-446655440000'; + const input: SyncBatchRequest = { + deviceId: 'pos-001', + items: [ + { + externalId: validUuid, + entityType: 'order', + payload: { customer: 'John Doe', total: 20.0 }, + }, + ], + }; + + ordersRepo.upsertByExternalId.mockResolvedValue({ status: 'duplicate' }); + + const { results } = await service.syncBatch(input); + + expect(results[0]).toEqual({ + externalId: validUuid, + status: 'duplicate', + }); + }); + + it('should handle invalid items', async () => { + // Note: validation usually happens in the Pipe, but the service should handle it if passed + const validUuid = '550e8400-e29b-41d4-a716-446655440000'; + const input: SyncBatchRequest = { + deviceId: 'pos-001', + items: [ + { + externalId: validUuid, + entityType: 'order', + payload: { customer: 'John Doe', total: 20.0 }, + }, + ], + }; + + ordersRepo.upsertByExternalId.mockResolvedValue({ status: 'invalid' }); + + const { results } = await service.syncBatch(input); + + expect(results[0].status).toBe('invalid'); + }); + + it('should handle auth_required items', async () => { + const validUuid = '550e8400-e29b-41d4-a716-446655440000'; + const input: SyncBatchRequest = { + deviceId: 'pos-001', + items: [ + { + externalId: validUuid, + entityType: 'order', + payload: { customer: 'John Doe', total: 20.0 }, + }, + ], + }; + + ordersRepo.upsertByExternalId.mockResolvedValue({ + status: 'auth_required', + }); + + const { results } = await service.syncBatch(input); + + expect(results[0].status).toBe('auth_required'); + }); }); }); diff --git a/apps/web/README.md b/apps/web/README.md index e215bc4..1ecb0e2 100644 --- a/apps/web/README.md +++ b/apps/web/README.md @@ -1,36 +1,18 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# @offline-pos/web -## Getting Started +Next.js frontend application providing the POS dashboard and offline synchronization engine. -First, run the development server: +## πŸ“‘ Responsibilities +- **Offline First**: Uses IndexedDB to allow order creation without internet. +- **Sync Runner**: Background process that manages the synchronization queue states. +- **Observability**: Real-time visualization of sync progress and queue health. -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` +## πŸ› οΈ Key Scripts +- `npm run dev`: Starts the development server. +- `npm run test`: Runs integration tests using Vitest. +- `npm run build`: Compiles the optimized production build. -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 [Geist](https://vercel.com/font), a new font family for Vercel. - -## 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. +## πŸ—οΈ Architecture +- **Sync Engine**: Located in `src/lib/sync/`. Handles enqueueing, retries, and batching. +- **IndexedDB**: Managed in `src/lib/db.ts`. Stores orders and the sync queue. +- **Components**: Tailwind-styled React components for the POS UI. diff --git a/apps/web/src/lib/sync/__tests__/runner.test.ts b/apps/web/src/lib/sync/__tests__/runner.test.ts index 23ced91..1a0bfb1 100644 --- a/apps/web/src/lib/sync/__tests__/runner.test.ts +++ b/apps/web/src/lib/sync/__tests__/runner.test.ts @@ -159,4 +159,77 @@ describe('runSyncOnce', () => { const recoveredItem = await db.get('syncQueue', '1'); expect(recoveredItem?.status).toBe('SYNCED'); }); + + it('should transition to DEAD_LETTER after max retries', async () => { + const db = await getDB(); + const now = Date.now(); + + const tx = db.transaction('syncQueue', 'readwrite'); + await tx.objectStore('syncQueue').add({ + id: '1', + externalId: 'ext-dead', + entityType: 'order', + status: 'PENDING', + nextAttemptAt: now - 1000, + createdAt: now - 2000, + payload: { data: 'fail' }, + url: 'v1/pos/sync', + method: 'POST', + op: 'UPSERT', + retryCount: 10 // Max retries reached + }); + await tx.done; + + const mockFetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ + results: [{ externalId: 'ext-dead', status: 'error', reason: 'permanent_fail' }] + }) + }); + + const result = await runSyncOnce({ + batchSize: 10, + fetchImpl: mockFetch as unknown as typeof fetch, + maxRetries: 10 + }); + + expect(result.dead).toBe(1); + + const item = await db.get('syncQueue', '1'); + expect(item?.status).toBe('DEAD_LETTER'); + expect(item?.lastError).toBe('permanent_fail'); + }); + + it('should handle network errors with exponential backoff', async () => { + const db = await getDB(); + const now = Date.now(); + + const tx = db.transaction('syncQueue', 'readwrite'); + await tx.objectStore('syncQueue').add({ + id: '1', + externalId: 'ext-net', + entityType: 'order', + status: 'PENDING', + nextAttemptAt: now - 1000, + createdAt: now - 2000, + payload: { data: 'net' }, + url: 'v1/pos/sync', + method: 'POST', + op: 'UPSERT', + retryCount: 0 + }); + await tx.done; + + const mockFetch = vi.fn().mockRejectedValue(new Error('Network timeout')); + + await runSyncOnce({ + batchSize: 10, + fetchImpl: mockFetch as unknown as typeof fetch + }); + + const item = await db.get('syncQueue', '1'); + expect(item?.status).toBe('RETRYABLE_ERROR'); + expect(item?.retryCount).toBe(1); + expect(item?.nextAttemptAt).toBeGreaterThan(now); + }); }); diff --git a/docs/README.md b/docs/README.md index f8cefe1..d9dfa7e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -12,10 +12,10 @@ This directory contains the detailed documentation for the Offline-First POS Sys β”œβ”€β”€ packages/ β”‚ └── sync-contract/ # Shared Zod schemas & TS types β”œβ”€β”€ infra/ -β”‚ β”œβ”€β”€ db/ # DB init scripts (for local dev) β”‚ └── docker-compose.yml # Orchestration └── docs/ β”œβ”€β”€ ADR/ # Architectural Decision Records + β”œβ”€β”€ RUNBOOK.md # Operational Guide └── README.md # You are here ``` diff --git a/docs/RUNBOOK.md b/docs/RUNBOOK.md new file mode 100644 index 0000000..9b77ab6 --- /dev/null +++ b/docs/RUNBOOK.md @@ -0,0 +1,47 @@ +# Operational Runbook - Offline-First POS + +## πŸ› οΈ Infrastructure Operations + +### Start All Services (Production-like) +```bash +docker-compose -f infra/docker-compose.yml up -d +``` + +### Database Maintenance +- **Check Migration Status**: `npx prisma migrate status -w apps/api` +- **Deploy New Migrations**: `npx prisma migrate deploy -w apps/api` +- **Reset DB (Dev only)**: `npx prisma migrate reset -w apps/api` + +## πŸ“Š Monitoring & Observability + +### Health Checks +- **API**: `GET http://localhost:3001/` (Should return 200 OK) +- **Sync Status**: `GET http://localhost:3001/v1/pos/sync` (Not allowed, but verifies endpoint exists) + +### Logs +- **Backend Logs**: `docker logs -f infra_api_1` +- **Frontend Sync Metrics**: Open Browser DevTools -> Application -> IndexedDB -> `pos-db` -> `syncQueue`. Check items with status `PENDING` or `ERROR`. + +## πŸ”„ Troubleshooting + +### Stuck Sync Queue +If items are stuck in `IN_FLIGHT` for more than 1 minute: +1. Refresh the web app. The runner will automatically requeue stale items. +2. Verify API connectivity: `ping localhost:3001`. + +### Database Connection Failure +If the API fails to start with `P1001`: +1. Check if PostgreSQL container is running: `docker ps`. +2. Ensure `DATABASE_URL` in `apps/api/.env` matches the `docker-compose.yml` service name. + +### Invalid Contracts +If the API returns `400 Bad Request` during sync: +1. Verify the shared contract version: `npm run build -w packages/sync-contract`. +2. Check `apps/api` logs for specific Zod validation errors. + +## πŸš€ Deployment Checklist +1. [ ] Build shared contract. +2. [ ] Run all tests (`npm run test`). +3. [ ] Run typecheck (`npm run typecheck`). +4. [ ] Tag the commit and push to main. +5. [ ] CI/CD pipeline will automatically create a release draft. From 9094d958308f61def2e0975df19883a4e67d3632 Mon Sep 17 00:00:00 2001 From: Salazarismo Date: Tue, 17 Mar 2026 17:41:49 -0300 Subject: [PATCH 6/8] fix: use PAT_GITHUB instead of GITHUB_TOKEN for release creation --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 07dc030..031de6b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,4 +59,4 @@ jobs: draft: true generate_release_notes: true env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.PAT_GITHUB }} From ef5cbe19ff583f3b749368e25e616d74b4f67305 Mon Sep 17 00:00:00 2001 From: Salazarismo Date: Thu, 19 Mar 2026 21:23:26 -0300 Subject: [PATCH 7/8] fix(ci): use PERSONAL_ACCESS_TOKEN for GitHub release permissions --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 031de6b..165e16a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,4 +59,4 @@ jobs: draft: true generate_release_notes: true env: - GITHUB_TOKEN: ${{ secrets.PAT_GITHUB }} + GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} From 08a489beb7c6a971494ae1396913cce4ad5e48ad Mon Sep 17 00:00:00 2001 From: Salazarismo Date: Fri, 20 Mar 2026 20:48:12 -0300 Subject: [PATCH 8/8] fix(ci): release only on tags with proper permissions --- .github/workflows/ci.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 165e16a..1bd7cfa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,9 +3,14 @@ name: CI on: push: branches: [ main ] + tags: + - 'v*' pull_request: branches: [ main ] +permissions: + contents: read + jobs: quality-gates: runs-on: ubuntu-latest @@ -49,8 +54,10 @@ jobs: release: needs: quality-gates - if: github.event_name == 'push' && github.ref == 'refs/heads/main' + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && github.repository == 'IndexGrid/offline-first-sync-queue' runs-on: ubuntu-latest + permissions: + contents: write steps: - uses: actions/checkout@v4 - name: Create Release Draft @@ -59,4 +66,4 @@ jobs: draft: true generate_release_notes: true env: - GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}