From 5cb31324ee7a9baf8c321422dd98a71086389615 Mon Sep 17 00:00:00 2001 From: Morten Olsen Date: Tue, 4 Nov 2025 11:51:51 +0100 Subject: [PATCH] feat: support views as data catalogs --- packages/server/package.json | 4 + packages/server/src/api/api.documents.ts | 2 +- packages/server/src/api/api.ts | 17 +++ packages/server/src/api/api.views.ts | 86 ++++++++++++++ packages/server/src/dev.ts | 8 ++ .../src/services/views/views.schemas.ts | 35 ++++++ packages/server/src/services/views/views.ts | 100 ++++++++++++++++ pnpm-lock.yaml | 110 ++++++++++++++++++ 8 files changed, 361 insertions(+), 1 deletion(-) create mode 100644 packages/server/src/api/api.views.ts create mode 100644 packages/server/src/dev.ts create mode 100644 packages/server/src/services/views/views.schemas.ts create mode 100644 packages/server/src/services/views/views.ts diff --git a/packages/server/package.json b/packages/server/package.json index 6635263..7505635 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -2,6 +2,7 @@ "type": "module", "main": "dist/exports.js", "scripts": { + "dev": "node --no-warnings --watch src/dev.ts", "build": "tsc --build", "test:unit": "vitest --run --passWithNoTests", "test": "pnpm run \"/^test:/\"" @@ -18,6 +19,7 @@ "@morten-olsen/reservoir-tests": "workspace:*", "@types/node": "24.10.0", "@vitest/coverage-v8": "4.0.6", + "dotenv": "^17.2.3", "typescript": "5.9.3", "vitest": "4.0.6" }, @@ -27,6 +29,7 @@ "#root/*": "./src/*" }, "dependencies": { + "@fastify/sensible": "^6.0.3", "@fastify/swagger": "^9.5.2", "@scalar/fastify-api-reference": "^1.38.1", "better-sqlite3": "^12.4.1", @@ -38,6 +41,7 @@ "pg": "^8.16.3", "pino": "^10.1.0", "pino-pretty": "^13.1.2", + "yaml": "^2.8.1", "zod": "^4.1.12" } } diff --git a/packages/server/src/api/api.documents.ts b/packages/server/src/api/api.documents.ts index 6d2a393..34ba73d 100644 --- a/packages/server/src/api/api.documents.ts +++ b/packages/server/src/api/api.documents.ts @@ -11,7 +11,7 @@ const documentsPlugin: FastifyPluginAsyncZod = async (app) => { method: 'POST', url: '', schema: { - operationId: 'v1.documents.post', + operationId: 'v1.documents.upsert', tags: ['documents'], summary: 'Upsert documents', body: upsertDocumentRequestSchema, diff --git a/packages/server/src/api/api.ts b/packages/server/src/api/api.ts index eb8cf6e..48dfeec 100644 --- a/packages/server/src/api/api.ts +++ b/packages/server/src/api/api.ts @@ -1,9 +1,12 @@ import fastify from 'fastify'; +import fastifySensible from '@fastify/sensible'; import fastifySwagger from '@fastify/swagger'; import fastifyScalar from '@scalar/fastify-api-reference'; +import YAML from 'yaml'; import { jsonSchemaTransform, serializerCompiler, validatorCompiler } from 'fastify-type-provider-zod'; import { documentsPlugin } from './api.documents.ts'; +import { viewsPlugin } from './api.views.ts'; import { Services } from '#root/utils/utils.services.ts'; import { DatabaseService } from '#root/database/database.ts'; @@ -22,6 +25,16 @@ const createApi = async (services: Services = new Services()) => { app.setValidatorCompiler(validatorCompiler); app.setSerializerCompiler(serializerCompiler); app.decorate('services', services); + app.addContentTypeParser('application/yaml', { parseAs: 'buffer' }, (_, body, done) => { + try { + const parsed = YAML.parse(body.toString('utf8')); // Parse the buffer as YAML + done(null, parsed); // Call done with null for error and the parsed object + } catch { + done(app.httpErrors.badRequest('Invalid YAML format')); + } + }); + + await app.register(fastifySensible); await app.register(fastifySwagger, { openapi: { @@ -42,6 +55,10 @@ const createApi = async (services: Services = new Services()) => { prefix: '/api/v1/documents', }); + await app.register(viewsPlugin, { + prefix: '/api/v1/views', + }); + app.addHook('onReady', async () => { app.swagger(); }); diff --git a/packages/server/src/api/api.views.ts b/packages/server/src/api/api.views.ts new file mode 100644 index 0000000..248640c --- /dev/null +++ b/packages/server/src/api/api.views.ts @@ -0,0 +1,86 @@ +import type { FastifyPluginAsyncZod } from 'fastify-type-provider-zod'; +import { z } from 'zod'; + +import { viewInfoSchema, viewUpsertRequestSchema } from '#root/services/views/views.schemas.ts'; +import { ViewsService } from '#root/services/views/views.ts'; + +const viewsPlugin: FastifyPluginAsyncZod = async (app) => { + app.route({ + method: 'GET', + url: '', + schema: { + operationId: 'v1.views.list', + tags: ['views'], + summary: 'List views', + response: { + 200: z.object({ + items: z.array(viewInfoSchema), + }), + }, + }, + handler: async (req, reply) => { + const viewsService = app.services.get(ViewsService); + const items = await viewsService.list(); + return reply.send({ items }); + }, + }); + + app.route({ + method: 'PUT', + url: '/:name', + schema: { + operationId: 'v1.views.list', + tags: ['views'], + summary: 'Upsert view', + params: z.object({ + name: z.string(), + }), + body: viewUpsertRequestSchema.omit({ + name: true, + }), + response: { + 200: z.object({ + name: z.string(), + }), + }, + }, + handler: async (req, reply) => { + const input = { + ...req.body, + name: req.params.name, + }; + const viewsService = app.services.get(ViewsService); + await viewsService.upsert(input); + reply.send({ + name: req.params.name, + }); + }, + }); + + app.route({ + method: 'DELETE', + url: '/:name', + schema: { + operationId: 'v1.views.delete', + tags: ['views'], + summary: 'Delete view', + params: z.object({ + name: z.string(), + }), + response: { + 200: z.object({ + name: z.string(), + }), + }, + }, + handler: async (req, reply) => { + const viewsService = app.services.get(ViewsService); + await viewsService.remove(req.params.name); + reply.send({ + name: req.params.name, + }); + }, + }); +}; + +export { viewsPlugin }; diff --git a/packages/server/src/dev.ts b/packages/server/src/dev.ts new file mode 100644 index 0000000..829d633 --- /dev/null +++ b/packages/server/src/dev.ts @@ -0,0 +1,8 @@ +import 'dotenv/config'; +import { createApi } from './api/api.ts'; + +const app = await createApi(); +await app.listen({ + port: 9111, + host: process.env.SERVER_HOST, +}); diff --git a/packages/server/src/services/views/views.schemas.ts b/packages/server/src/services/views/views.schemas.ts new file mode 100644 index 0000000..9a49d36 --- /dev/null +++ b/packages/server/src/services/views/views.schemas.ts @@ -0,0 +1,35 @@ +import { z } from 'zod'; + +const columnInfoSchema = z.object({ + name: z.string(), + dataType: z.string(), + isNullable: z.boolean(), +}); + +type ColumnInfo = z.infer; + +const viewInfoSchema = z.object({ + name: z.string(), + columns: z.array(columnInfoSchema), +}); + +type ViewInfo = z.infer; + +const viewUpsertRequestSchema = z.object({ + name: z.string(), + description: z.string().nullable(), + columns: z + .record( + z.string(), + z.object({ + description: z.string().nullish(), + }), + ) + .optional(), + query: z.string(), +}); + +type ViewUpsertRequest = z.infer; + +export type { ColumnInfo, ViewInfo, ViewUpsertRequest }; +export { columnInfoSchema, viewInfoSchema, viewUpsertRequestSchema }; diff --git a/packages/server/src/services/views/views.ts b/packages/server/src/services/views/views.ts new file mode 100644 index 0000000..56ee751 --- /dev/null +++ b/packages/server/src/services/views/views.ts @@ -0,0 +1,100 @@ +import type { ViewInfo, ViewUpsertRequest } from './views.schemas.ts'; + +import { DatabaseService } from '#root/database/database.ts'; +import type { Services } from '#root/utils/utils.services.ts'; + +class ViewsService { + #services: Services; + + constructor(services: Services) { + this.#services = services; + } + + #listFromPostgres = async () => { + const dbService = this.#services.get(DatabaseService); + const db = await dbService.getInstance(); + const pgViewsResult = await db.raw<{ rows: { schema_name: string; view_name: string }[] }>(` + SELECT + schemaname AS schema_name, + viewname AS view_name + FROM + pg_views + WHERE + schemaname NOT IN ('pg_catalog', 'information_schema', 'information_schema') + AND viewname NOT LIKE 'pg_%' + AND viewname NOT LIKE 'sql_%'; + `); + const views: ViewInfo[] = await Promise.all( + pgViewsResult.rows.map(async (row) => { + const pgColumnsResult = await db.raw<{ + rows: { column_name: string; data_type: string; is_nullable: 'YES' | 'NO' }[]; + }>( + ` + SELECT + column_name, + data_type, + is_nullable + FROM + information_schema.columns + WHERE + table_schema = ? + AND table_name = ? + ORDER BY + ordinal_position; + `, + [row.schema_name, row.view_name], + ); + const result: ViewInfo = { + name: row.view_name, + columns: pgColumnsResult.rows.map((column) => ({ + name: column.column_name, + dataType: column.data_type, + isNullable: column.is_nullable === 'YES', + })), + }; + return result; + }), + ); + return views; + }; + + public list = async () => { + const dbService = this.#services.get(DatabaseService); + const db = await dbService.getInstance(); + + switch (db.client.config.client) { + case 'pg': + return this.#listFromPostgres(); + default: + throw new Error(`Client ${db.client} is not supported`); + } + }; + + public upsert = async (options: ViewUpsertRequest) => { + const { name, columns = {}, query, description } = options; + const dbService = this.#services.get(DatabaseService); + const db = await dbService.getInstance(); + const subquery = db.raw(query); + await db.schema.createViewOrReplace(name, (view) => { + // view.columns(columns); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + view.as(subquery as any); + }); + if (description) { + const sql = db.raw(`COMMENT ON VIEW ?? IS ?;`, [name, description]); + await db.raw(sql.toQuery()); + } + for (const [columnName, info] of Object.entries(columns)) { + const sql = db.raw(`COMMENT ON COLUMN ??.?? IS ?;`, [name, columnName, info.description || null]); + await db.raw(sql.toQuery()); + } + }; + + public remove = async (name: string) => { + const dbService = this.#services.get(DatabaseService); + const db = await dbService.getInstance(); + await db.schema.dropViewIfExists(name); + }; +} + +export { ViewsService }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc252c9..c85b518 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -52,6 +52,9 @@ importers: packages/server: dependencies: + '@fastify/sensible': + specifier: ^6.0.3 + version: 6.0.3 '@fastify/swagger': specifier: ^9.5.2 version: 9.5.2 @@ -85,6 +88,9 @@ importers: pino-pretty: specifier: ^13.1.2 version: 13.1.2 + yaml: + specifier: ^2.8.1 + version: 2.8.1 zod: specifier: ^4.1.12 version: 4.1.12 @@ -101,6 +107,9 @@ importers: '@vitest/coverage-v8': specifier: 4.0.6 version: 4.0.6(vitest@4.0.6(@types/node@24.10.0)(yaml@2.8.1)) + dotenv: + specifier: ^17.2.3 + version: 17.2.3 typescript: specifier: 5.9.3 version: 5.9.3 @@ -369,6 +378,9 @@ packages: '@fastify/proxy-addr@5.1.0': resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} + '@fastify/sensible@6.0.3': + resolution: {integrity: sha512-Iyn8698hp/e5+v8SNBBruTa7UfrMEP52R16dc9jMpqSyEcPsvWFQo+R6WwHCUnJiLIsuci2ZoEZ7ilrSSCPIVg==} + '@fastify/swagger@9.5.2': resolution: {integrity: sha512-8e8w/LItg/cF6IR/hYKtnt+E0QImees5o3YWJsTLxaIk+tzNUEc6Z2Ursi4oOHWwUlKjUCnV6yh5z5ZdxvlsWA==} @@ -402,6 +414,10 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@lukeed/ms@2.0.2': + resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} + engines: {node: '>=8'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1141,6 +1157,10 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -1153,6 +1173,10 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} + dotenv@17.2.3: + resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} + engines: {node: '>=12'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -1421,6 +1445,10 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -1537,6 +1565,10 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -1863,6 +1895,10 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + mem@8.1.1: resolution: {integrity: sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==} engines: {node: '>=10'} @@ -1878,6 +1914,14 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -2334,6 +2378,9 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2402,6 +2449,10 @@ packages: stacktracey@2.1.8: resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==} + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} @@ -2522,6 +2573,10 @@ packages: resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} engines: {node: '>=12'} + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + ts-api-utils@2.1.0: resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} engines: {node: '>=18.12'} @@ -2587,6 +2642,10 @@ packages: resolution: {integrity: sha512-GeJop7+u7BYlQ6yQCAY1nBQiRSHR+6OdCEtd8Bwp9a3NK3+fWAVjOaPKJDteB9f6cIJ0wt4IfnScjLG450EpXA==} engines: {node: '>=20'} + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -2632,6 +2691,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + vite@7.1.12: resolution: {integrity: sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2957,6 +3020,16 @@ snapshots: '@fastify/forwarded': 3.0.1 ipaddr.js: 2.2.0 + '@fastify/sensible@6.0.3': + dependencies: + '@lukeed/ms': 2.0.2 + dequal: 2.0.3 + fastify-plugin: 5.1.0 + forwarded: 0.2.0 + http-errors: 2.0.0 + type-is: 1.6.18 + vary: 1.1.2 + '@fastify/swagger@9.5.2': dependencies: fastify-plugin: 5.1.0 @@ -2989,6 +3062,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@lukeed/ms@2.0.2': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -3854,6 +3929,8 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + depd@2.0.0: {} + dequal@2.0.3: {} detect-libc@2.1.2: {} @@ -3862,6 +3939,8 @@ snapshots: dependencies: esutils: 2.0.3 + dotenv@17.2.3: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -4252,6 +4331,8 @@ snapshots: dependencies: is-callable: 1.2.7 + forwarded@0.2.0: {} + fs-constants@1.0.0: {} fsevents@2.3.3: @@ -4360,6 +4441,14 @@ snapshots: html-escaper@2.0.2: {} + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + human-signals@2.1.0: {} ieee754@1.2.1: {} @@ -4660,6 +4749,8 @@ snapshots: math-intrinsics@1.1.0: {} + media-typer@0.3.0: {} + mem@8.1.1: dependencies: map-age-cleaner: 0.1.3 @@ -4674,6 +4765,12 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + mimic-fn@2.1.0: {} mimic-fn@3.1.0: {} @@ -5167,6 +5264,8 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.1 + setprototypeof@1.2.0: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -5240,6 +5339,8 @@ snapshots: as-table: 1.0.55 get-source: 2.0.12 + statuses@2.0.1: {} + std-env@3.10.0: {} stop-iteration-iterator@1.1.0: @@ -5359,6 +5460,8 @@ snapshots: toad-cache@3.7.0: {} + toidentifier@1.0.1: {} + ts-api-utils@2.1.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -5415,6 +5518,11 @@ snapshots: dependencies: tagged-tag: 1.0.0 + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -5480,6 +5588,8 @@ snapshots: util-deprecate@1.0.2: {} + vary@1.1.2: {} + vite@7.1.12(@types/node@24.10.0)(yaml@2.8.1): dependencies: esbuild: 0.25.12