5 Commits
0.1.2 ... 0.1.7

Author SHA1 Message Date
Morten Olsen
e3398e1968 fix: use view recreate 2025-11-04 13:17:44 +01:00
Morten Olsen
5cb31324ee feat: support views as data catalogs 2025-11-04 11:51:51 +01:00
Morten Olsen
daa816ac61 fix: postgres object compat 2025-11-03 20:04:35 +01:00
Morten Olsen
75d24c31c2 feat: add ingestion queue 2025-11-03 17:14:27 +01:00
Morten Olsen
2f542c3066 simplify post endpoint 2025-11-03 17:10:33 +01:00
11 changed files with 454 additions and 66 deletions

View File

@@ -2,6 +2,7 @@
"type": "module", "type": "module",
"main": "dist/exports.js", "main": "dist/exports.js",
"scripts": { "scripts": {
"dev": "node --no-warnings --watch src/dev.ts",
"build": "tsc --build", "build": "tsc --build",
"test:unit": "vitest --run --passWithNoTests", "test:unit": "vitest --run --passWithNoTests",
"test": "pnpm run \"/^test:/\"" "test": "pnpm run \"/^test:/\""
@@ -18,6 +19,7 @@
"@morten-olsen/reservoir-tests": "workspace:*", "@morten-olsen/reservoir-tests": "workspace:*",
"@types/node": "24.10.0", "@types/node": "24.10.0",
"@vitest/coverage-v8": "4.0.6", "@vitest/coverage-v8": "4.0.6",
"dotenv": "^17.2.3",
"typescript": "5.9.3", "typescript": "5.9.3",
"vitest": "4.0.6" "vitest": "4.0.6"
}, },
@@ -27,6 +29,7 @@
"#root/*": "./src/*" "#root/*": "./src/*"
}, },
"dependencies": { "dependencies": {
"@fastify/sensible": "^6.0.3",
"@fastify/swagger": "^9.5.2", "@fastify/swagger": "^9.5.2",
"@scalar/fastify-api-reference": "^1.38.1", "@scalar/fastify-api-reference": "^1.38.1",
"better-sqlite3": "^12.4.1", "better-sqlite3": "^12.4.1",
@@ -34,9 +37,11 @@
"fastify": "^5.6.1", "fastify": "^5.6.1",
"fastify-type-provider-zod": "^6.1.0", "fastify-type-provider-zod": "^6.1.0",
"knex": "^3.1.0", "knex": "^3.1.0",
"p-queue": "^9.0.0",
"pg": "^8.16.3", "pg": "^8.16.3",
"pino": "^10.1.0", "pino": "^10.1.0",
"pino-pretty": "^13.1.2", "pino-pretty": "^13.1.2",
"yaml": "^2.8.1",
"zod": "^4.1.12" "zod": "^4.1.12"
} }
} }

View File

@@ -1,4 +1,3 @@
import { z } from 'zod';
import type { FastifyPluginAsyncZod } from 'fastify-type-provider-zod'; import type { FastifyPluginAsyncZod } from 'fastify-type-provider-zod';
import { DocumentsService } from '#root/services/documents/documents.ts'; import { DocumentsService } from '#root/services/documents/documents.ts';
@@ -12,23 +11,18 @@ const documentsPlugin: FastifyPluginAsyncZod = async (app) => {
method: 'POST', method: 'POST',
url: '', url: '',
schema: { schema: {
operationId: 'v1.documents.put', operationId: 'v1.documents.upsert',
tags: ['documents'], tags: ['documents'],
summary: 'Upsert documents', summary: 'Upsert documents',
body: z.object({ body: upsertDocumentRequestSchema,
items: z.array(upsertDocumentRequestSchema),
}),
response: { response: {
200: z.object({ 200: upsertDocumentResponseSchema,
items: z.array(upsertDocumentResponseSchema),
}),
}, },
}, },
handler: async (req, reply) => { handler: async (req, reply) => {
const documentsService = app.services.get(DocumentsService); const documentsService = app.services.get(DocumentsService);
const { items } = req.body; const result = await documentsService.upsert(req.body);
const results = await Promise.all(items.map((item) => documentsService.upsert(item))); return reply.send(result);
return reply.send({ items: results });
}, },
}); });
}; };

View File

@@ -1,9 +1,12 @@
import fastify from 'fastify'; import fastify from 'fastify';
import fastifySensible from '@fastify/sensible';
import fastifySwagger from '@fastify/swagger'; import fastifySwagger from '@fastify/swagger';
import fastifyScalar from '@scalar/fastify-api-reference'; import fastifyScalar from '@scalar/fastify-api-reference';
import YAML from 'yaml';
import { jsonSchemaTransform, serializerCompiler, validatorCompiler } from 'fastify-type-provider-zod'; import { jsonSchemaTransform, serializerCompiler, validatorCompiler } from 'fastify-type-provider-zod';
import { documentsPlugin } from './api.documents.ts'; import { documentsPlugin } from './api.documents.ts';
import { viewsPlugin } from './api.views.ts';
import { Services } from '#root/utils/utils.services.ts'; import { Services } from '#root/utils/utils.services.ts';
import { DatabaseService } from '#root/database/database.ts'; import { DatabaseService } from '#root/database/database.ts';
@@ -22,6 +25,16 @@ const createApi = async (services: Services = new Services()) => {
app.setValidatorCompiler(validatorCompiler); app.setValidatorCompiler(validatorCompiler);
app.setSerializerCompiler(serializerCompiler); app.setSerializerCompiler(serializerCompiler);
app.decorate('services', services); 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, { await app.register(fastifySwagger, {
openapi: { openapi: {
@@ -42,6 +55,10 @@ const createApi = async (services: Services = new Services()) => {
prefix: '/api/v1/documents', prefix: '/api/v1/documents',
}); });
await app.register(viewsPlugin, {
prefix: '/api/v1/views',
});
app.addHook('onReady', async () => { app.addHook('onReady', async () => {
app.swagger(); app.swagger();
}); });

View File

@@ -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 };

View File

@@ -27,10 +27,10 @@ type DocumentRow = {
id: string; id: string;
type: string; type: string;
source: string | null; source: string | null;
data: string; data: string | unknown;
createdAt: string; createdAt: string | Date;
updatedAt: string; updatedAt: string | Date;
deletedAt: string | null; deletedAt: string | Date | null;
}; };
type Tables = { type Tables = {

View File

@@ -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,
});

View File

@@ -1,4 +1,5 @@
import equal from 'fast-deep-equal'; import equal from 'fast-deep-equal';
import Queue from 'p-queue';
import type { UpsertDocumentRequest, UpsertDocumentResponse } from './documents.schemas.ts'; import type { UpsertDocumentRequest, UpsertDocumentResponse } from './documents.schemas.ts';
@@ -7,71 +8,76 @@ import type { Services } from '#root/utils/utils.services.ts';
class DocumentsService { class DocumentsService {
#services: Services; #services: Services;
#queue: Queue;
constructor(services: Services) { constructor(services: Services) {
this.#services = services; this.#services = services;
this.#queue = new Queue({
concurrency: 10,
});
} }
public upsert = async (document: UpsertDocumentRequest): Promise<UpsertDocumentResponse> => { public upsert = (document: UpsertDocumentRequest): Promise<UpsertDocumentResponse> =>
const dbService = this.#services.get(DatabaseService); this.#queue.add(async () => {
const db = await dbService.getInstance(); const dbService = this.#services.get(DatabaseService);
const db = await dbService.getInstance();
const id = document.id || crypto.randomUUID(); const id = document.id || crypto.randomUUID();
const [current] = await db<Tables['document']>(tableNames).where({ const [current] = await db<Tables['document']>(tableNames).where({
id,
type: document.type,
});
const now = new Date();
if (!current) {
await db<Tables['document']>(tableNames.documents).insert({
id, id,
type: document.type, type: document.type,
createdAt: now.toISOString(),
updatedAt: now.toISOString(),
data: JSON.stringify(document.data),
}); });
return { const now = new Date();
data: document.data,
id, if (!current) {
type: document.type, await db<Tables['document']>(tableNames.documents).insert({
source: document.source || null, id,
createdAt: now.toISOString(), type: document.type,
updatedAt: now.toISOString(), createdAt: now.toISOString(),
deletedAt: null, updatedAt: now.toISOString(),
action: 'inserted', data: JSON.stringify(document.data),
}; });
} return {
const currentData = JSON.parse(current.data); data: document.data,
if (equal(currentData, document.data)) { id,
type: document.type,
source: document.source || null,
createdAt: now.toISOString(),
updatedAt: now.toISOString(),
deletedAt: null,
action: 'inserted',
};
}
const currentData = typeof current.data === 'string' ? JSON.parse(current.data) : current.data;
if (equal(currentData, document.data)) {
return {
...current,
data: currentData,
id,
createdAt: new Date(current.createdAt).toISOString(),
updatedAt: new Date(current.updatedAt).toISOString(),
deletedAt: current.deletedAt ? new Date(current.deletedAt).toISOString() : null,
action: 'skipped',
};
}
await db<Tables['document']>(tableNames.documents)
.update({
source: document.source,
data: JSON.stringify(document.data),
updatedAt: now.toISOString(),
})
.where({ id, type: document.type });
return { return {
...current, ...current,
data: currentData,
id, id,
createdAt: current.createdAt, data: document.data,
updatedAt: current.updatedAt, createdAt: new Date(current.createdAt).toISOString(),
deletedAt: current.deletedAt || null,
action: 'skipped',
};
}
await db<Tables['document']>(tableNames.documents)
.update({
source: document.source,
data: JSON.stringify(document.data),
updatedAt: now.toISOString(), updatedAt: now.toISOString(),
}) deletedAt: current.deletedAt ? new Date(current.deletedAt).toISOString() : null,
.where({ id, type: document.type }); action: 'updated',
return { };
...current, });
id,
data: document.data,
createdAt: current.createdAt,
updatedAt: now.toISOString(),
deletedAt: current.deletedAt || null,
action: 'updated',
};
};
} }
export { DocumentsService }; export { DocumentsService };

View File

@@ -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<typeof columnInfoSchema>;
const viewInfoSchema = z.object({
name: z.string(),
columns: z.array(columnInfoSchema),
});
type ViewInfo = z.infer<typeof viewInfoSchema>;
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<typeof viewUpsertRequestSchema>;
export type { ColumnInfo, ViewInfo, ViewUpsertRequest };
export { columnInfoSchema, viewInfoSchema, viewUpsertRequestSchema };

View File

@@ -0,0 +1,103 @@
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.transaction(async (trx) => {
await trx.schema.dropViewIfExists(name);
await trx.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 = trx.raw(`COMMENT ON VIEW ?? IS ?;`, [name, description]);
await trx.raw(sql.toQuery());
}
for (const [columnName, info] of Object.entries(columns)) {
const sql = trx.raw(`COMMENT ON COLUMN ??.?? IS ?;`, [name, columnName, info.description || null]);
await trx.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 };

133
pnpm-lock.yaml generated
View File

@@ -52,6 +52,9 @@ importers:
packages/server: packages/server:
dependencies: dependencies:
'@fastify/sensible':
specifier: ^6.0.3
version: 6.0.3
'@fastify/swagger': '@fastify/swagger':
specifier: ^9.5.2 specifier: ^9.5.2
version: 9.5.2 version: 9.5.2
@@ -73,6 +76,9 @@ importers:
knex: knex:
specifier: ^3.1.0 specifier: ^3.1.0
version: 3.1.0(better-sqlite3@12.4.1)(pg@8.16.3) version: 3.1.0(better-sqlite3@12.4.1)(pg@8.16.3)
p-queue:
specifier: ^9.0.0
version: 9.0.0
pg: pg:
specifier: ^8.16.3 specifier: ^8.16.3
version: 8.16.3 version: 8.16.3
@@ -82,6 +88,9 @@ importers:
pino-pretty: pino-pretty:
specifier: ^13.1.2 specifier: ^13.1.2
version: 13.1.2 version: 13.1.2
yaml:
specifier: ^2.8.1
version: 2.8.1
zod: zod:
specifier: ^4.1.12 specifier: ^4.1.12
version: 4.1.12 version: 4.1.12
@@ -98,6 +107,9 @@ importers:
'@vitest/coverage-v8': '@vitest/coverage-v8':
specifier: 4.0.6 specifier: 4.0.6
version: 4.0.6(vitest@4.0.6(@types/node@24.10.0)(yaml@2.8.1)) 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: typescript:
specifier: 5.9.3 specifier: 5.9.3
version: 5.9.3 version: 5.9.3
@@ -366,6 +378,9 @@ packages:
'@fastify/proxy-addr@5.1.0': '@fastify/proxy-addr@5.1.0':
resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} 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': '@fastify/swagger@9.5.2':
resolution: {integrity: sha512-8e8w/LItg/cF6IR/hYKtnt+E0QImees5o3YWJsTLxaIk+tzNUEc6Z2Ursi4oOHWwUlKjUCnV6yh5z5ZdxvlsWA==} resolution: {integrity: sha512-8e8w/LItg/cF6IR/hYKtnt+E0QImees5o3YWJsTLxaIk+tzNUEc6Z2Ursi4oOHWwUlKjUCnV6yh5z5ZdxvlsWA==}
@@ -399,6 +414,10 @@ packages:
'@jridgewell/trace-mapping@0.3.31': '@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} 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': '@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@@ -1138,6 +1157,10 @@ packages:
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
dequal@2.0.3: dequal@2.0.3:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -1150,6 +1173,10 @@ packages:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
engines: {node: '>=0.10.0'} 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: dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -1310,6 +1337,9 @@ packages:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
eventemitter3@5.0.1:
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
execa@5.1.1: execa@5.1.1:
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -1415,6 +1445,10 @@ packages:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
forwarded@0.2.0:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
engines: {node: '>= 0.6'}
fs-constants@1.0.0: fs-constants@1.0.0:
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
@@ -1531,6 +1565,10 @@ packages:
html-escaper@2.0.2: html-escaper@2.0.2:
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} 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: human-signals@2.1.0:
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
engines: {node: '>=10.17.0'} engines: {node: '>=10.17.0'}
@@ -1857,6 +1895,10 @@ packages:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
media-typer@0.3.0:
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
engines: {node: '>= 0.6'}
mem@8.1.1: mem@8.1.1:
resolution: {integrity: sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==} resolution: {integrity: sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -1872,6 +1914,14 @@ packages:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'} 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: mimic-fn@2.1.0:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -2005,6 +2055,14 @@ packages:
resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==}
engines: {node: '>=6'} engines: {node: '>=6'}
p-queue@9.0.0:
resolution: {integrity: sha512-KO1RyxstL9g1mK76530TExamZC/S2Glm080Nx8PE5sTd7nlduDQsAfEl4uXX+qZjLiwvDauvzXavufy3+rJ9zQ==}
engines: {node: '>=20'}
p-timeout@7.0.1:
resolution: {integrity: sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==}
engines: {node: '>=20'}
parent-module@1.0.1: parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -2320,6 +2378,9 @@ packages:
resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
shebang-command@2.0.0: shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -2388,6 +2449,10 @@ packages:
stacktracey@2.1.8: stacktracey@2.1.8:
resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==} 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: std-env@3.10.0:
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
@@ -2508,6 +2573,10 @@ packages:
resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==}
engines: {node: '>=12'} 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: ts-api-utils@2.1.0:
resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==}
engines: {node: '>=18.12'} engines: {node: '>=18.12'}
@@ -2573,6 +2642,10 @@ packages:
resolution: {integrity: sha512-GeJop7+u7BYlQ6yQCAY1nBQiRSHR+6OdCEtd8Bwp9a3NK3+fWAVjOaPKJDteB9f6cIJ0wt4IfnScjLG450EpXA==} resolution: {integrity: sha512-GeJop7+u7BYlQ6yQCAY1nBQiRSHR+6OdCEtd8Bwp9a3NK3+fWAVjOaPKJDteB9f6cIJ0wt4IfnScjLG450EpXA==}
engines: {node: '>=20'} engines: {node: '>=20'}
type-is@1.6.18:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
engines: {node: '>= 0.6'}
typed-array-buffer@1.0.3: typed-array-buffer@1.0.3:
resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -2618,6 +2691,10 @@ packages:
util-deprecate@1.0.2: util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} 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: vite@7.1.12:
resolution: {integrity: sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==} resolution: {integrity: sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
@@ -2943,6 +3020,16 @@ snapshots:
'@fastify/forwarded': 3.0.1 '@fastify/forwarded': 3.0.1
ipaddr.js: 2.2.0 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': '@fastify/swagger@9.5.2':
dependencies: dependencies:
fastify-plugin: 5.1.0 fastify-plugin: 5.1.0
@@ -2975,6 +3062,8 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2 '@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
'@lukeed/ms@2.0.2': {}
'@nodelib/fs.scandir@2.1.5': '@nodelib/fs.scandir@2.1.5':
dependencies: dependencies:
'@nodelib/fs.stat': 2.0.5 '@nodelib/fs.stat': 2.0.5
@@ -3840,6 +3929,8 @@ snapshots:
has-property-descriptors: 1.0.2 has-property-descriptors: 1.0.2
object-keys: 1.1.1 object-keys: 1.1.1
depd@2.0.0: {}
dequal@2.0.3: {} dequal@2.0.3: {}
detect-libc@2.1.2: {} detect-libc@2.1.2: {}
@@ -3848,6 +3939,8 @@ snapshots:
dependencies: dependencies:
esutils: 2.0.3 esutils: 2.0.3
dotenv@17.2.3: {}
dunder-proto@1.0.1: dunder-proto@1.0.1:
dependencies: dependencies:
call-bind-apply-helpers: 1.0.2 call-bind-apply-helpers: 1.0.2
@@ -4113,6 +4206,8 @@ snapshots:
esutils@2.0.3: {} esutils@2.0.3: {}
eventemitter3@5.0.1: {}
execa@5.1.1: execa@5.1.1:
dependencies: dependencies:
cross-spawn: 7.0.6 cross-spawn: 7.0.6
@@ -4236,6 +4331,8 @@ snapshots:
dependencies: dependencies:
is-callable: 1.2.7 is-callable: 1.2.7
forwarded@0.2.0: {}
fs-constants@1.0.0: {} fs-constants@1.0.0: {}
fsevents@2.3.3: fsevents@2.3.3:
@@ -4344,6 +4441,14 @@ snapshots:
html-escaper@2.0.2: {} 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: {} human-signals@2.1.0: {}
ieee754@1.2.1: {} ieee754@1.2.1: {}
@@ -4644,6 +4749,8 @@ snapshots:
math-intrinsics@1.1.0: {} math-intrinsics@1.1.0: {}
media-typer@0.3.0: {}
mem@8.1.1: mem@8.1.1:
dependencies: dependencies:
map-age-cleaner: 0.1.3 map-age-cleaner: 0.1.3
@@ -4658,6 +4765,12 @@ snapshots:
braces: 3.0.3 braces: 3.0.3
picomatch: 2.3.1 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@2.1.0: {}
mimic-fn@3.1.0: {} mimic-fn@3.1.0: {}
@@ -4784,6 +4897,13 @@ snapshots:
p-map@2.1.0: {} p-map@2.1.0: {}
p-queue@9.0.0:
dependencies:
eventemitter3: 5.0.1
p-timeout: 7.0.1
p-timeout@7.0.1: {}
parent-module@1.0.1: parent-module@1.0.1:
dependencies: dependencies:
callsites: 3.1.0 callsites: 3.1.0
@@ -5144,6 +5264,8 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
es-object-atoms: 1.1.1 es-object-atoms: 1.1.1
setprototypeof@1.2.0: {}
shebang-command@2.0.0: shebang-command@2.0.0:
dependencies: dependencies:
shebang-regex: 3.0.0 shebang-regex: 3.0.0
@@ -5217,6 +5339,8 @@ snapshots:
as-table: 1.0.55 as-table: 1.0.55
get-source: 2.0.12 get-source: 2.0.12
statuses@2.0.1: {}
std-env@3.10.0: {} std-env@3.10.0: {}
stop-iteration-iterator@1.1.0: stop-iteration-iterator@1.1.0:
@@ -5336,6 +5460,8 @@ snapshots:
toad-cache@3.7.0: {} toad-cache@3.7.0: {}
toidentifier@1.0.1: {}
ts-api-utils@2.1.0(typescript@5.9.3): ts-api-utils@2.1.0(typescript@5.9.3):
dependencies: dependencies:
typescript: 5.9.3 typescript: 5.9.3
@@ -5392,6 +5518,11 @@ snapshots:
dependencies: dependencies:
tagged-tag: 1.0.0 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: typed-array-buffer@1.0.3:
dependencies: dependencies:
call-bound: 1.0.4 call-bound: 1.0.4
@@ -5457,6 +5588,8 @@ snapshots:
util-deprecate@1.0.2: {} util-deprecate@1.0.2: {}
vary@1.1.2: {}
vite@7.1.12(@types/node@24.10.0)(yaml@2.8.1): vite@7.1.12(@types/node@24.10.0)(yaml@2.8.1):
dependencies: dependencies:
esbuild: 0.25.12 esbuild: 0.25.12

View File

@@ -3,3 +3,4 @@ packages:
- ./apps/* - ./apps/*
onlyBuiltDependencies: onlyBuiltDependencies:
- better-sqlite3 - better-sqlite3
- esbuild