test: backups service
This commit is contained in:
parent
94f49e2022
commit
760327a0b3
16 changed files with 276 additions and 14 deletions
1
.env.test
Normal file
1
.env.test
Normal file
|
|
@ -0,0 +1 @@
|
|||
DATABASE_URL=:memory:
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
export const OPERATION_TIMEOUT = 5000;
|
||||
export const VOLUME_MOUNT_BASE = "/var/lib/zerobyte/volumes";
|
||||
export const REPOSITORY_BASE = "/var/lib/zerobyte/repositories";
|
||||
export const DATABASE_URL = "/var/lib/zerobyte/data/ironmount.db";
|
||||
export const DATABASE_URL = process.env.DATABASE_URL || "/var/lib/zerobyte/data/ironmount.db";
|
||||
export const RESTIC_PASS_FILE = "/var/lib/zerobyte/data/restic.pass";
|
||||
|
||||
export const DEFAULT_EXCLUDES = [DATABASE_URL, RESTIC_PASS_FILE, REPOSITORY_BASE];
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export const volumesTable = sqliteTable("volumes_table", {
|
|||
autoRemount: int("auto_remount", { mode: "boolean" }).notNull().default(true),
|
||||
});
|
||||
export type Volume = typeof volumesTable.$inferSelect;
|
||||
export type VolumeInsert = typeof volumesTable.$inferInsert;
|
||||
|
||||
/**
|
||||
* Users Table
|
||||
|
|
@ -61,6 +62,7 @@ export const repositoriesTable = sqliteTable("repositories_table", {
|
|||
updatedAt: int("updated_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
|
||||
});
|
||||
export type Repository = typeof repositoriesTable.$inferSelect;
|
||||
export type RepositoryInsert = typeof repositoriesTable.$inferInsert;
|
||||
|
||||
/**
|
||||
* Backup Schedules Table
|
||||
|
|
@ -96,6 +98,7 @@ export const backupSchedulesTable = sqliteTable("backup_schedules_table", {
|
|||
createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
|
||||
updatedAt: int("updated_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
|
||||
});
|
||||
export type BackupScheduleInsert = typeof backupSchedulesTable.$inferInsert;
|
||||
|
||||
export const backupScheduleRelations = relations(backupSchedulesTable, ({ one, many }) => ({
|
||||
volume: one(volumesTable, {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { Job } from "../core/scheduler";
|
||||
import { backupsService } from "../modules/backups/backups.service";
|
||||
import { toMessage } from "../utils/errors";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
export class BackupExecutionJob extends Job {
|
||||
|
|
@ -17,9 +16,7 @@ export class BackupExecutionJob extends Job {
|
|||
logger.info(`Found ${scheduleIds.length} backup schedule(s) to execute`);
|
||||
|
||||
for (const scheduleId of scheduleIds) {
|
||||
backupsService.executeBackup(scheduleId).catch((error) => {
|
||||
logger.error(`Failed to execute backup for schedule ${scheduleId}: ${toMessage(error)}`);
|
||||
});
|
||||
backupsService.executeBackup(scheduleId);
|
||||
}
|
||||
|
||||
return { done: true, timestamp: new Date(), executed: scheduleIds.length };
|
||||
|
|
|
|||
151
app/server/modules/backups/__tests__/backups.service.test.ts
Normal file
151
app/server/modules/backups/__tests__/backups.service.test.ts
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
import { test, describe, mock, expect } from "bun:test";
|
||||
import { backupsService } from "../backups.service";
|
||||
import { createTestVolume } from "~/test/helpers/volume";
|
||||
import { createTestBackupSchedule } from "~/test/helpers/backup";
|
||||
import { createTestRepository } from "~/test/helpers/repository";
|
||||
import { generateBackupOutput } from "~/test/helpers/restic";
|
||||
import { beforeEach } from "bun:test";
|
||||
|
||||
const resticBackupMock = mock(() => Promise.resolve({ exitCode: 0 }));
|
||||
|
||||
mock.module("~/server/utils/spawn", () => ({
|
||||
safeSpawn: resticBackupMock,
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
resticBackupMock.mockClear();
|
||||
});
|
||||
|
||||
describe("execute backup", () => {
|
||||
test("should correctly set next backup time", async () => {
|
||||
// arrange
|
||||
const volume = await createTestVolume();
|
||||
const repository = await createTestRepository();
|
||||
const schedule = await createTestBackupSchedule({
|
||||
volumeId: volume.id,
|
||||
repositoryId: repository.id,
|
||||
cronExpression: "*/5 * * * *",
|
||||
});
|
||||
expect(schedule.nextBackupAt).toBeNull();
|
||||
|
||||
resticBackupMock.mockImplementationOnce(() =>
|
||||
Promise.resolve({ exitCode: 0, stdout: generateBackupOutput(), stderr: "" }),
|
||||
);
|
||||
|
||||
// act
|
||||
await backupsService.executeBackup(schedule.id);
|
||||
|
||||
// assert
|
||||
const updatedSchedule = await backupsService.getSchedule(schedule.id);
|
||||
expect(updatedSchedule.nextBackupAt).not.toBeNull();
|
||||
|
||||
const nextBackupAt = new Date(updatedSchedule.nextBackupAt ?? 0);
|
||||
const now = new Date();
|
||||
|
||||
expect(nextBackupAt.getTime()).toBeGreaterThanOrEqual(now.getTime());
|
||||
expect(nextBackupAt.getTime() - now.getTime()).toBeLessThanOrEqual(5 * 60 * 1000);
|
||||
});
|
||||
|
||||
test("should skip backup if schedule is disabled", async () => {
|
||||
// arrange
|
||||
const volume = await createTestVolume();
|
||||
const repository = await createTestRepository();
|
||||
const schedule = await createTestBackupSchedule({
|
||||
volumeId: volume.id,
|
||||
repositoryId: repository.id,
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
// act
|
||||
await backupsService.executeBackup(schedule.id);
|
||||
|
||||
// assert
|
||||
expect(resticBackupMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should execute backup if schedule is disabled but the run is manual", async () => {
|
||||
// arrange
|
||||
const volume = await createTestVolume();
|
||||
const repository = await createTestRepository();
|
||||
const schedule = await createTestBackupSchedule({
|
||||
volumeId: volume.id,
|
||||
repositoryId: repository.id,
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
resticBackupMock.mockImplementationOnce(() =>
|
||||
Promise.resolve({ exitCode: 0, stdout: generateBackupOutput(), stderr: "" }),
|
||||
);
|
||||
|
||||
// act
|
||||
await backupsService.executeBackup(schedule.id, true);
|
||||
|
||||
// assert
|
||||
expect(resticBackupMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should skip the backup if the previous one is still running", async () => {
|
||||
// arrange
|
||||
const volume = await createTestVolume();
|
||||
const repository = await createTestRepository();
|
||||
const schedule = await createTestBackupSchedule({
|
||||
volumeId: volume.id,
|
||||
repositoryId: repository.id,
|
||||
});
|
||||
|
||||
resticBackupMock.mockImplementation(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
return Promise.resolve({ exitCode: 0, stdout: generateBackupOutput(), stderr: "" });
|
||||
});
|
||||
|
||||
// act
|
||||
backupsService.executeBackup(schedule.id);
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
await backupsService.executeBackup(schedule.id);
|
||||
|
||||
// assert
|
||||
expect(resticBackupMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("should set the backup status to failed if restic returns a 3 exit code", async () => {
|
||||
// arrange
|
||||
const volume = await createTestVolume();
|
||||
const repository = await createTestRepository();
|
||||
const schedule = await createTestBackupSchedule({
|
||||
volumeId: volume.id,
|
||||
repositoryId: repository.id,
|
||||
});
|
||||
|
||||
resticBackupMock.mockImplementationOnce(() =>
|
||||
Promise.resolve({ exitCode: 3, stdout: generateBackupOutput(), stderr: "Some error occurred" }),
|
||||
);
|
||||
|
||||
// act
|
||||
await backupsService.executeBackup(schedule.id);
|
||||
|
||||
// assert
|
||||
const updatedSchedule = await backupsService.getSchedule(schedule.id);
|
||||
expect(updatedSchedule.lastBackupStatus).toBe("warning");
|
||||
});
|
||||
|
||||
test("should set the backup status to failed if restic returns a non zero exit code", async () => {
|
||||
// arrange
|
||||
const volume = await createTestVolume();
|
||||
const repository = await createTestRepository();
|
||||
const schedule = await createTestBackupSchedule({
|
||||
volumeId: volume.id,
|
||||
repositoryId: repository.id,
|
||||
});
|
||||
|
||||
resticBackupMock.mockImplementationOnce(() =>
|
||||
Promise.resolve({ exitCode: 1, stdout: generateBackupOutput(), stderr: "Some error occurred" }),
|
||||
);
|
||||
|
||||
// act
|
||||
await backupsService.executeBackup(schedule.id);
|
||||
|
||||
// assert
|
||||
const updatedSchedule = await backupsService.getSchedule(schedule.id);
|
||||
expect(updatedSchedule.lastBackupStatus).toBe("error");
|
||||
});
|
||||
});
|
||||
|
|
@ -86,9 +86,7 @@ export const backupScheduleController = new Hono()
|
|||
.post("/:scheduleId/run", runBackupNowDto, async (c) => {
|
||||
const scheduleId = c.req.param("scheduleId");
|
||||
|
||||
backupsService.executeBackup(Number(scheduleId), true).catch((error) => {
|
||||
console.error("Backup execution failed:", error);
|
||||
});
|
||||
backupsService.executeBackup(Number(scheduleId), true);
|
||||
|
||||
return c.json<RunBackupNowDto>({ success: true }, 200);
|
||||
})
|
||||
|
|
|
|||
|
|
@ -363,8 +363,6 @@ const executeBackup = async (scheduleId: number, manual = false) => {
|
|||
.catch((notifError) => {
|
||||
logger.error(`Failed to send backup failure notification: ${toMessage(notifError)}`);
|
||||
});
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
runningBackups.delete(scheduleId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ const backupOutputSchema = type({
|
|||
total_duration: "number",
|
||||
snapshot_id: "string",
|
||||
});
|
||||
export type BackupOutput = typeof backupOutputSchema.infer;
|
||||
|
||||
const snapshotInfoSchema = type({
|
||||
gid: "number?",
|
||||
|
|
@ -344,7 +345,7 @@ const backup = async (
|
|||
throw new ResticError(res.exitCode, res.stderr.toString());
|
||||
}
|
||||
|
||||
const lastLine = stdout.trim();
|
||||
const lastLine = (stdout || res.stdout).trim();
|
||||
let summaryLine = "";
|
||||
try {
|
||||
const resSummary = JSON.parse(lastLine ?? "{}");
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { spawn } from "node:child_process";
|
||||
|
||||
interface Params {
|
||||
export interface SafeSpawnParams {
|
||||
command: string;
|
||||
args: string[];
|
||||
env?: NodeJS.ProcessEnv;
|
||||
|
|
@ -18,7 +18,7 @@ type SpawnResult = {
|
|||
stderr: string;
|
||||
};
|
||||
|
||||
export const safeSpawn = (params: Params) => {
|
||||
export const safeSpawn = (params: SafeSpawnParams) => {
|
||||
const { command, args, env = {}, signal, ...callbacks } = params;
|
||||
|
||||
return new Promise<SpawnResult>((resolve) => {
|
||||
|
|
|
|||
16
app/test/helpers/backup.ts
Normal file
16
app/test/helpers/backup.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { db } from "~/server/db/db";
|
||||
import { faker } from "@faker-js/faker";
|
||||
import { backupSchedulesTable, type BackupScheduleInsert } from "~/server/db/schema";
|
||||
|
||||
export const createTestBackupSchedule = async (overrides: Partial<BackupScheduleInsert> = {}) => {
|
||||
const backup: BackupScheduleInsert = {
|
||||
name: faker.system.fileName(),
|
||||
cronExpression: "0 0 * * *",
|
||||
repositoryId: "repo_123",
|
||||
volumeId: 1,
|
||||
...overrides,
|
||||
};
|
||||
|
||||
const data = await db.insert(backupSchedulesTable).values(backup).returning();
|
||||
return data[0];
|
||||
};
|
||||
20
app/test/helpers/repository.ts
Normal file
20
app/test/helpers/repository.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { db } from "~/server/db/db";
|
||||
import { faker } from "@faker-js/faker";
|
||||
import { repositoriesTable, type RepositoryInsert } from "~/server/db/schema";
|
||||
|
||||
export const createTestRepository = async (overrides: Partial<RepositoryInsert> = {}) => {
|
||||
const repository: RepositoryInsert = {
|
||||
id: faker.string.alphanumeric(6),
|
||||
name: faker.string.alphanumeric(10),
|
||||
shortId: faker.string.alphanumeric(6),
|
||||
config: {
|
||||
name: "test-repo",
|
||||
backend: "local",
|
||||
},
|
||||
type: "local",
|
||||
...overrides,
|
||||
};
|
||||
|
||||
const data = await db.insert(repositoriesTable).values(repository).returning();
|
||||
return data[0];
|
||||
};
|
||||
18
app/test/helpers/restic.ts
Normal file
18
app/test/helpers/restic.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
export const generateBackupOutput = () => {
|
||||
return JSON.stringify({
|
||||
message_type: "summary",
|
||||
files_new: 10,
|
||||
files_changed: 5,
|
||||
files_unmodified: 85,
|
||||
dirs_new: 2,
|
||||
dirs_changed: 1,
|
||||
dirs_unmodified: 17,
|
||||
data_blobs: 20,
|
||||
tree_blobs: 5,
|
||||
data_added: 1048576,
|
||||
total_files_processed: 100,
|
||||
total_bytes_processed: 2097152,
|
||||
total_duration: 12.34,
|
||||
snapshot_id: "abcd1234",
|
||||
});
|
||||
};
|
||||
21
app/test/helpers/volume.ts
Normal file
21
app/test/helpers/volume.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { db } from "~/server/db/db";
|
||||
import { faker } from "@faker-js/faker";
|
||||
import { volumesTable, type VolumeInsert } from "~/server/db/schema";
|
||||
|
||||
export const createTestVolume = async (overrides: Partial<VolumeInsert> = {}) => {
|
||||
const volume: VolumeInsert = {
|
||||
name: faker.system.fileName(),
|
||||
config: {
|
||||
backend: "directory",
|
||||
path: `/mnt/volumes/${faker.system.fileName()}`,
|
||||
},
|
||||
status: "mounted",
|
||||
autoRemount: true,
|
||||
shortId: faker.string.alphanumeric(6),
|
||||
type: "directory",
|
||||
...overrides,
|
||||
};
|
||||
|
||||
const data = await db.insert(volumesTable).values(volume).returning();
|
||||
return data[0];
|
||||
};
|
||||
19
app/test/setup.ts
Normal file
19
app/test/setup.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { beforeAll, mock } from "bun:test";
|
||||
import { migrate } from "drizzle-orm/bun-sqlite/migrator";
|
||||
import path from "node:path";
|
||||
import { cwd } from "node:process";
|
||||
import { db } from "~/server/db/db";
|
||||
|
||||
mock.module("~/server/utils/logger", () => ({
|
||||
logger: {
|
||||
debug: () => {},
|
||||
info: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
},
|
||||
}));
|
||||
|
||||
beforeAll(async () => {
|
||||
const migrationsFolder = path.join(cwd(), "app", "drizzle");
|
||||
migrate(db, { migrationsFolder });
|
||||
});
|
||||
16
bun.lock
16
bun.lock
|
|
@ -58,6 +58,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.3.8",
|
||||
"@faker-js/faker": "^10.1.0",
|
||||
"@hey-api/openapi-ts": "^0.88.0",
|
||||
"@react-router/dev": "^7.10.0",
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
|
|
@ -66,6 +67,7 @@
|
|||
"@types/node": "^24.10.1",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"dotenv-cli": "^11.0.0",
|
||||
"drizzle-kit": "^0.31.7",
|
||||
"lightningcss": "^1.30.2",
|
||||
"tailwindcss": "^4.1.17",
|
||||
|
|
@ -230,6 +232,8 @@
|
|||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="],
|
||||
|
||||
"@faker-js/faker": ["@faker-js/faker@10.1.0", "", {}, "sha512-C3mrr3b5dRVlKPJdfrAXS8+dq+rq8Qm5SNRazca0JKgw1HQERFmrVb0towvMmw5uu8hHKNiQasMaR/tydf3Zsg=="],
|
||||
|
||||
"@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="],
|
||||
|
||||
"@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="],
|
||||
|
|
@ -578,6 +582,8 @@
|
|||
|
||||
"cron-parser": ["cron-parser@5.4.0", "", { "dependencies": { "luxon": "^3.7.1" } }, "sha512-HxYB8vTvnQFx4dLsZpGRa0uHp6X3qIzS3ZJgJ9v6l/5TJMgeWQbLkR5yiJ5hOxGbc9+jCADDnydIe15ReLZnJA=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||
|
||||
"d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
|
||||
|
|
@ -748,6 +754,8 @@
|
|||
|
||||
"isbot": ["isbot@5.1.32", "", {}, "sha512-VNfjM73zz2IBZmdShMfAUg10prm6t7HFUQmNAEOAVS4YH92ZrZcvkMcGX6cIgBJAzWDzPent/EeAtYEHNPNPBQ=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
|
@ -812,6 +820,8 @@
|
|||
|
||||
"minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||
|
||||
"morgan": ["morgan@1.10.1", "", { "dependencies": { "basic-auth": "~2.0.1", "debug": "2.6.9", "depd": "~2.0.0", "on-finished": "~2.3.0", "on-headers": "~1.1.0" } }, "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
|
@ -848,6 +858,8 @@
|
|||
|
||||
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="],
|
||||
|
||||
"pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
|
||||
|
|
@ -936,6 +948,10 @@
|
|||
|
||||
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
||||
|
||||
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@
|
|||
"start:prod": "docker compose down && docker compose up --build zerobyte-prod",
|
||||
"gen:api-client": "openapi-ts",
|
||||
"gen:migrations": "drizzle-kit generate",
|
||||
"studio": "drizzle-kit studio"
|
||||
"studio": "drizzle-kit studio",
|
||||
"test": "dotenv -e .env.test -- bun test --preload ./app/test/setup.ts"
|
||||
},
|
||||
"overrides": {
|
||||
"esbuild": "^0.27.2"
|
||||
|
|
@ -73,6 +74,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.3.8",
|
||||
"@faker-js/faker": "^10.1.0",
|
||||
"@hey-api/openapi-ts": "^0.88.0",
|
||||
"@react-router/dev": "^7.10.0",
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
|
|
@ -81,6 +83,7 @@
|
|||
"@types/node": "^24.10.1",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"dotenv-cli": "^11.0.0",
|
||||
"drizzle-kit": "^0.31.7",
|
||||
"lightningcss": "^1.30.2",
|
||||
"tailwindcss": "^4.1.17",
|
||||
|
|
|
|||
Loading…
Reference in a new issue