@zaaxch/tailframe 4.0.5 → 4.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/new.mjs +57 -15
- package/src/owned-sources.mjs +27 -16
- package/src/service-templates.mjs +15 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zaaxch/tailframe",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.6",
|
|
4
4
|
"description": "Tailframe architecture toolkit: validates the Tailframe structure, import-boundary, and file-convention contracts. The package version is the contract version.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/new.mjs
CHANGED
|
@@ -292,7 +292,7 @@ export const env = {
|
|
|
292
292
|
firebaseServiceAccountPath: process.env.FIREBASE_SERVICE_ACCOUNT_PATH,` : ""}
|
|
293
293
|
${postgres ? `postgresUri: process.env.POSTGRES_URI ?? "postgresql://${options.name}-app:development@localhost:5432/${databaseName}",` : `mongodbUri: process.env.MONGODB_URI ?? "mongodb://localhost:27017/?replicaSet=rs0&directConnection=true",
|
|
294
294
|
mongodbDbName: process.env.MONGODB_DB_NAME ?? "${databaseName}",`}
|
|
295
|
-
${options.redis ? `redisUrl: process.env.REDIS_URL ?? "redis://localhost:6379"
|
|
295
|
+
${options.redis ? `redisUrl: process.env.REDIS_URL ?? "redis://localhost:6379"` : ""}
|
|
296
296
|
};`);
|
|
297
297
|
const databaseSource = postgres ? `import { Pool } from "pg";
|
|
298
298
|
import { env } from "@/platform/config/env";
|
|
@@ -358,15 +358,36 @@ if (options.worker) add(`${svc}/src/worker.ts`, `import "reflect-metadata";\nimp
|
|
|
358
358
|
add(`${svc}/src/modules/health/__tests__/GetHealth.test.ts`, getHealthTestSource);
|
|
359
359
|
add(`${svc}/src/modules/health/__tests__/GetReadiness.test.ts`, getReadinessTestSource);
|
|
360
360
|
add(`${svc}/src/__tests__/testDb.ts`, postgres ? `import { Pool } from "pg";
|
|
361
|
-
const pool = new Pool({
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
export async function
|
|
361
|
+
const pool = new Pool({
|
|
362
|
+
connectionString: process.env.TEST_POSTGRES_URI ?? "postgresql://postgres:postgres@localhost:5433/${databaseName}_test"
|
|
363
|
+
});
|
|
364
|
+
export async function openTestDatabase() {
|
|
365
|
+
await pool.query("SELECT 1");
|
|
366
|
+
return pool;
|
|
367
|
+
}
|
|
368
|
+
export async function resetTestDatabase() {
|
|
369
|
+
await pool.query("DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public");
|
|
370
|
+
return pool;
|
|
371
|
+
}
|
|
372
|
+
export async function closeTestDatabase() {
|
|
373
|
+
await pool.end();
|
|
374
|
+
}
|
|
365
375
|
` : `import { MongoClient } from "mongodb";
|
|
366
|
-
const client = new MongoClient(
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
export async function
|
|
376
|
+
const client = new MongoClient(
|
|
377
|
+
process.env.TEST_MONGODB_URI ?? "mongodb://localhost:27018/?replicaSet=rs0&directConnection=true"
|
|
378
|
+
);
|
|
379
|
+
export async function openTestDatabase() {
|
|
380
|
+
await client.connect();
|
|
381
|
+
return client.db(process.env.TEST_MONGODB_DB_NAME ?? "${options.name.replaceAll("-", "_")}_test");
|
|
382
|
+
}
|
|
383
|
+
export async function resetTestDatabase() {
|
|
384
|
+
const database = await openTestDatabase();
|
|
385
|
+
await database.dropDatabase();
|
|
386
|
+
return database;
|
|
387
|
+
}
|
|
388
|
+
export async function closeTestDatabase() {
|
|
389
|
+
await client.close();
|
|
390
|
+
}
|
|
370
391
|
`);
|
|
371
392
|
add(`${svc}/docker-compose.test.yml`, postgres ? `name: ${options.name}-test
|
|
372
393
|
services:
|
|
@@ -468,13 +489,20 @@ ${csrf ? `
|
|
|
468
489
|
` : ""}${options.ui ? `
|
|
469
490
|
it("serves the production SPA without falling through for API paths", async () => {
|
|
470
491
|
const app = createApplication({ production: true, publicPath: path.join(__dirname, "fixtures/public") });
|
|
471
|
-
await request(app)
|
|
492
|
+
await request(app)
|
|
493
|
+
.get("/dashboard")
|
|
494
|
+
.expect(200)
|
|
495
|
+
.expect(({ text }) => expect(text).toContain("boundary-test-spa"));
|
|
472
496
|
await request(app).get("/api/v1/does-not-exist").expect(404);
|
|
473
|
-
})
|
|
474
|
-
` : ""}
|
|
497
|
+
});` : ""}
|
|
475
498
|
});
|
|
476
499
|
`);
|
|
477
|
-
if (options.ui) add(`${svc}/src/app/__tests__/fixtures/public/index.html`, `<!doctype html
|
|
500
|
+
if (options.ui) add(`${svc}/src/app/__tests__/fixtures/public/index.html`, `<!doctype html>
|
|
501
|
+
<html>
|
|
502
|
+
<body>
|
|
503
|
+
<div id="app">boundary-test-spa</div>
|
|
504
|
+
</body>
|
|
505
|
+
</html>`);
|
|
478
506
|
add(`${svc}/AGENTS.md`, `# ${title} service guidance
|
|
479
507
|
|
|
480
508
|
## Scope
|
|
@@ -659,8 +687,22 @@ import NotificationHost from "@/app/components/NotificationHost.vue";
|
|
|
659
687
|
</script>`);
|
|
660
688
|
add(`${ui}/src/app/startApplication.ts`, `import { createApp } from "vue";\nimport { createPinia } from "pinia";\nimport PrimeVue from "primevue/config";\nimport App from "@/app/App.vue";\n${options.auth === "firebase" ? 'import { configureHttp } from "@/app/configureHttp";\n' : ""}import router from "@/app/router";\nimport "@/app/styles.css";\nexport function startApplication() {\n\tconst app = createApp(App);\n\tconst pinia = createPinia();\n\tapp.use(pinia).use(PrimeVue).use(router);\n\t${options.auth === "firebase" ? "configureHttp(pinia);\n\t" : ""}app.mount("#app");\n}\n`);
|
|
661
689
|
add(`${ui}/src/main.ts`, `import { startApplication } from "@/app/startApplication";\nstartApplication();\n`);
|
|
662
|
-
add(`${ui}/src/app/styles.css`, `@import "tailwindcss"
|
|
663
|
-
|
|
690
|
+
add(`${ui}/src/app/styles.css`, `@import "tailwindcss";
|
|
691
|
+
:root {
|
|
692
|
+
font-family: Inter, ui-sans-serif, system-ui;
|
|
693
|
+
color: #17221c;
|
|
694
|
+
background: #f5f3ec;
|
|
695
|
+
}
|
|
696
|
+
body {
|
|
697
|
+
margin: 0;
|
|
698
|
+
}`);
|
|
699
|
+
add(`${ui}/src/app/__tests__/App.spec.ts`, `import { mount } from "@vue/test-utils";
|
|
700
|
+
import App from "@/app/App.vue";
|
|
701
|
+
describe("App", () => {
|
|
702
|
+
it("renders a router view", () => {
|
|
703
|
+
expect(mount(App, { global: { stubs: ["RouterView", "NotificationHost"] } }).exists()).toBe(true);
|
|
704
|
+
});
|
|
705
|
+
});`);
|
|
664
706
|
add(`${ui}/AGENTS.md`, `# ${title} UI guidance
|
|
665
707
|
|
|
666
708
|
## Scope
|
package/src/owned-sources.mjs
CHANGED
|
@@ -80,7 +80,10 @@ const errorTranslationTestSource = `import { AppError, type FailureKind } from "
|
|
|
80
80
|
import { errorHandler } from "@/platform/http/errors";
|
|
81
81
|
import { logger } from "@/platform/logging/logger";
|
|
82
82
|
|
|
83
|
-
jest.mock("@/platform/logging/logger", () => ({
|
|
83
|
+
jest.mock("@/platform/logging/logger", () => ({
|
|
84
|
+
logger: { log: jest.fn() },
|
|
85
|
+
serializeError: (error: unknown) => error
|
|
86
|
+
}));
|
|
84
87
|
|
|
85
88
|
const log = logger.log as jest.MockedFunction<typeof logger.log>;
|
|
86
89
|
|
|
@@ -104,13 +107,15 @@ it.each(statuses)("translates %s to HTTP %i", (kind, status) => {
|
|
|
104
107
|
errorHandler(new AppError({ code: "CODE", message: "message", kind }), request, response, jest.fn());
|
|
105
108
|
expect((response as { status: jest.Mock }).status).toHaveBeenCalledWith(status);
|
|
106
109
|
expect(json).toHaveBeenCalledWith({ error: { code: "CODE", message: "message" } });
|
|
107
|
-
expect(log).toHaveBeenCalledWith(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
110
|
+
expect(log).toHaveBeenCalledWith(
|
|
111
|
+
expect.objectContaining({
|
|
112
|
+
level: status >= 500 ? "error" : "warn",
|
|
113
|
+
event: "http.request.failed",
|
|
114
|
+
requestId: "request-1",
|
|
115
|
+
code: "CODE",
|
|
116
|
+
kind
|
|
117
|
+
})
|
|
118
|
+
);
|
|
114
119
|
});
|
|
115
120
|
|
|
116
121
|
it("logs an unknown failure without exposing it to the client", () => {
|
|
@@ -120,11 +125,13 @@ it("logs an unknown failure without exposing it to the client", () => {
|
|
|
120
125
|
const error = new Error("database disconnected");
|
|
121
126
|
errorHandler(error, { method: "POST", path: "/health.get" } as never, response, jest.fn());
|
|
122
127
|
expect(json).toHaveBeenCalledWith({ error: { code: "INTERNAL", message: "Internal server error" } });
|
|
123
|
-
expect(log).toHaveBeenCalledWith(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
+
expect(log).toHaveBeenCalledWith(
|
|
129
|
+
expect.objectContaining({
|
|
130
|
+
level: "error",
|
|
131
|
+
code: "INTERNAL",
|
|
132
|
+
error: expect.objectContaining({ message: "database disconnected" })
|
|
133
|
+
})
|
|
134
|
+
);
|
|
128
135
|
});
|
|
129
136
|
|
|
130
137
|
it("logs a preserved application-error cause", () => {
|
|
@@ -138,9 +145,13 @@ it("logs a preserved application-error cause", () => {
|
|
|
138
145
|
response,
|
|
139
146
|
jest.fn()
|
|
140
147
|
);
|
|
141
|
-
expect(log).toHaveBeenCalledWith(
|
|
142
|
-
|
|
143
|
-
|
|
148
|
+
expect(log).toHaveBeenCalledWith(
|
|
149
|
+
expect.objectContaining({
|
|
150
|
+
error: expect.objectContaining({
|
|
151
|
+
cause: expect.objectContaining({ message: "Firebase token verification failed" })
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
);
|
|
144
155
|
});
|
|
145
156
|
`;
|
|
146
157
|
|
|
@@ -417,7 +417,15 @@ export function createRateLimiter(client: RedisClientType, policy: RateLimitPoli
|
|
|
417
417
|
if (error instanceof RateLimiterRes)
|
|
418
418
|
next(new AppError({ code: "RATE_LIMITED", message: "Too many requests", kind: "rate_limited" }));
|
|
419
419
|
else if (error instanceof AppError) next(error);
|
|
420
|
-
else
|
|
420
|
+
else
|
|
421
|
+
next(
|
|
422
|
+
new AppError({
|
|
423
|
+
code: "RATE_LIMIT_UNAVAILABLE",
|
|
424
|
+
message: "Request protection unavailable",
|
|
425
|
+
kind: "unavailable",
|
|
426
|
+
cause: error
|
|
427
|
+
})
|
|
428
|
+
);
|
|
421
429
|
}
|
|
422
430
|
};
|
|
423
431
|
}
|
|
@@ -499,7 +507,12 @@ export class GetReadiness implements UseCase<Record<string, never>, ReadinessRes
|
|
|
499
507
|
await Promise.all(this.probes.map((probe) => probe.check()));
|
|
500
508
|
return { status: "ready" };
|
|
501
509
|
} catch (error) {
|
|
502
|
-
throw new AppError({
|
|
510
|
+
throw new AppError({
|
|
511
|
+
code: "NOT_READY",
|
|
512
|
+
message: "Service is not ready",
|
|
513
|
+
kind: "unavailable",
|
|
514
|
+
cause: error
|
|
515
|
+
});
|
|
503
516
|
}
|
|
504
517
|
}
|
|
505
518
|
}
|