@tatara/cli 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/dist/bin.mjs +3001 -0
- package/package.json +32 -3
- package/templates/features/admin/files/src/apps/admin/app.ts +4 -0
- package/templates/features/admin/files/src/apps/admin/server.ts +5 -0
- package/templates/features/admin/files/src/modules/example/routes/admin-greeting.routes.ts +24 -0
- package/templates/features/admin/files/src/modules/example/tests/admin.test.ts +20 -0
- package/templates/features/auth/files/src/core/auth/middleware.ts +29 -0
- package/templates/features/auth/files/src/core/auth/routes/me.routes.ts +36 -0
- package/templates/features/auth/files/src/core/auth/tests/me.test.ts +25 -0
- package/templates/features/auth-orgs/files+drizzle+mysql/src/core/auth/db/organization.schema.ts +38 -0
- package/templates/features/auth-orgs/files+drizzle+postgres/src/core/auth/db/organization.schema.ts +38 -0
- package/templates/features/auth-orgs/files+prisma/prisma/models/organization.prisma +41 -0
- package/templates/features/authz/files/src/core/authz/cache.ts +31 -0
- package/templates/features/authz/files/src/core/authz/catalog.ts +85 -0
- package/templates/features/authz/files/src/core/authz/commands/grant-role.ts +21 -0
- package/templates/features/authz/files/src/core/authz/commands/sync-permissions.ts +11 -0
- package/templates/features/authz/files/src/core/authz/constants.ts +32 -0
- package/templates/features/authz/files/src/core/authz/db/seed.ts +10 -0
- package/templates/features/authz/files/src/core/authz/middleware.ts +21 -0
- package/templates/features/authz/files/src/core/authz/permissions.ts +31 -0
- package/templates/features/authz/files/src/core/authz/resolver.ts +98 -0
- package/templates/features/authz/files/src/core/authz/routes/authorization.routes.ts +281 -0
- package/templates/features/authz/files/src/core/authz/services/audit.ts +24 -0
- package/templates/features/authz/files/src/core/authz/services/grants.ts +29 -0
- package/templates/features/authz/files/src/core/authz/services/members.ts +84 -0
- package/templates/features/authz/files/src/core/authz/services/roles.ts +73 -0
- package/templates/features/authz/files/src/core/authz/services/sync.ts +34 -0
- package/templates/features/authz/files/src/core/authz/system-roles.ts +9 -0
- package/templates/features/authz/files/src/core/authz/tests/authz.test.ts +146 -0
- package/templates/features/authz/files/src/core/authz/tests/error-messages.test.ts +34 -0
- package/templates/features/authz/files/src/core/authz/tests/request-body.test.ts +27 -0
- package/templates/features/authz/files/src/core/authz/types.ts +87 -0
- package/templates/features/authz/files/src/core/authz/validators.ts +99 -0
- package/templates/features/authz/files/tests/helpers/authz.ts +30 -0
- package/templates/features/authz/files+drizzle/src/core/authz/db/store.ts +275 -0
- package/templates/features/authz/files+drizzle+mysql/src/core/authz/db/authz.schema.ts +90 -0
- package/templates/features/authz/files+drizzle+postgres/src/core/authz/db/authz.schema.ts +88 -0
- package/templates/features/authz/files+prisma/prisma/models/authz.prisma +75 -0
- package/templates/features/authz/files+prisma/src/core/authz/db/store.ts +236 -0
- package/templates/features/base/files/AGENTS.md +71 -0
- package/templates/features/base/files/CLAUDE.md +1 -0
- package/templates/features/base/files/Dockerfile +16 -0
- package/templates/features/base/files/README.md +31 -0
- package/templates/features/base/files/_github/workflows/ci.yml +26 -0
- package/templates/features/base/files/biome.template.json +20 -0
- package/templates/features/base/files/dockerignore +8 -0
- package/templates/features/base/files/gitignore +9 -0
- package/templates/features/base/files/src/apps/api/app.ts +3 -0
- package/templates/features/base/files/src/apps/api/server.ts +5 -0
- package/templates/features/base/files/src/apps/cli/index.ts +6 -0
- package/templates/features/base/files/src/core/commands/define.ts +33 -0
- package/templates/features/base/files/src/core/commands/runner.ts +54 -0
- package/templates/features/base/files/src/core/commands/tests/flags.test.ts +16 -0
- package/templates/features/base/files/src/core/config/tests/env.test.ts +40 -0
- package/templates/features/base/files/src/core/http/app.ts +57 -0
- package/templates/features/base/files/src/core/http/context.ts +18 -0
- package/templates/features/base/files/src/core/http/errors.ts +79 -0
- package/templates/features/base/files/src/core/http/health.ts +25 -0
- package/templates/features/base/files/src/core/http/middleware/error-handler.ts +36 -0
- package/templates/features/base/files/src/core/http/middleware/request-context.ts +24 -0
- package/templates/features/base/files/src/core/http/middleware/validation.ts +17 -0
- package/templates/features/base/files/src/core/http/response.ts +72 -0
- package/templates/features/base/files/src/core/http/routes.ts +28 -0
- package/templates/features/base/files/src/core/http/server.ts +15 -0
- package/templates/features/base/files/src/core/logging/logger.ts +17 -0
- package/templates/features/base/files/src/core/process/lifecycle.ts +34 -0
- package/templates/features/base/files/src/modules/example/routes/greeting.routes.ts +27 -0
- package/templates/features/base/files/src/modules/example/services/greeting.service.ts +3 -0
- package/templates/features/base/files/src/modules/example/tests/greeting.test.ts +21 -0
- package/templates/features/base/files/src/modules/example/validators/greeting.validators.ts +9 -0
- package/templates/features/base/files/tests/helpers/api.ts +54 -0
- package/templates/features/base/files/tests/setup.ts +2 -0
- package/templates/features/base/files/tsconfig.json +20 -0
- package/templates/features/base/files/tsdown.config.ts +15 -0
- package/templates/features/base/files/vitest.config.ts +16 -0
- package/templates/features/better-auth/files/src/core/auth/auth.ts +26 -0
- package/templates/features/better-auth/files/src/core/auth/db/seed.ts +24 -0
- package/templates/features/better-auth/files/src/core/auth/delivery.ts +48 -0
- package/templates/features/better-auth/files/src/core/auth/openapi.ts +60 -0
- package/templates/features/better-auth/files/src/core/auth/principal.ts +40 -0
- package/templates/features/better-auth/files/src/core/auth/sms.ts +14 -0
- package/templates/features/better-auth/files/src/core/auth/tests/openapi.test.ts +45 -0
- package/templates/features/better-auth/files/tests/helpers/auth.ts +40 -0
- package/templates/features/better-auth/files+admin/src/core/auth/tests/openapi-admin.test.ts +8 -0
- package/templates/features/better-auth/files+auth-orgs/src/core/auth/tests/principal.test.ts +44 -0
- package/templates/features/better-auth/files+auth-orgs+authz/src/core/auth/tests/principal-authz.test.ts +40 -0
- package/templates/features/better-auth/files+drizzle+mysql/src/core/auth/adapter.ts +15 -0
- package/templates/features/better-auth/files+drizzle+mysql/src/core/auth/db/auth.schema.ts +57 -0
- package/templates/features/better-auth/files+drizzle+postgres/src/core/auth/adapter.ts +15 -0
- package/templates/features/better-auth/files+drizzle+postgres/src/core/auth/db/auth.schema.ts +56 -0
- package/templates/features/better-auth/files+prisma/prisma/models/auth.prisma +62 -0
- package/templates/features/better-auth/files+prisma+mysql/src/core/auth/adapter.ts +4 -0
- package/templates/features/better-auth/files+prisma+postgres/src/core/auth/adapter.ts +4 -0
- package/templates/features/db/files/src/core/db/commands/migrate.ts +12 -0
- package/templates/features/db/files/src/core/db/commands/reset.ts +19 -0
- package/templates/features/db/files/src/core/db/commands/seed.ts +13 -0
- package/templates/features/db/files/src/core/db/id.ts +13 -0
- package/templates/features/db/files/src/core/db/seed.ts +14 -0
- package/templates/features/db/files/src/core/db/tests/reset.test.ts +7 -0
- package/templates/features/db/files/tests/global-setup.ts +27 -0
- package/templates/features/db/files/tests/helpers/database-url.ts +9 -0
- package/templates/features/db/files/tests/helpers/db.ts +7 -0
- package/templates/features/db/files+mysql+drizzle/src/core/db/admin.ts +53 -0
- package/templates/features/db/files+mysql+prisma/src/core/db/admin.ts +78 -0
- package/templates/features/db/files+postgres/src/core/db/admin.ts +50 -0
- package/templates/features/drizzle/files+mysql/drizzle.config.ts +12 -0
- package/templates/features/drizzle/files+mysql/src/core/db/client.ts +20 -0
- package/templates/features/drizzle/files+mysql/src/core/db/migrate.ts +13 -0
- package/templates/features/drizzle/files+postgres/drizzle.config.ts +12 -0
- package/templates/features/drizzle/files+postgres/src/core/db/client.ts +20 -0
- package/templates/features/drizzle/files+postgres/src/core/db/migrate.ts +13 -0
- package/templates/features/email/files/src/core/email/commands/send-test.ts +13 -0
- package/templates/features/email/files/src/core/email/send.ts +22 -0
- package/templates/features/email/files/src/core/email/tests/email.test.ts +8 -0
- package/templates/features/i18n/files/src/core/i18n/index.ts +41 -0
- package/templates/features/i18n/files/src/core/i18n/tests/i18n.test.ts +21 -0
- package/templates/features/i18n/files/src/locales/ar.json +10 -0
- package/templates/features/i18n/files/src/locales/en.json +10 -0
- package/templates/features/jobs/files/src/apps/cron/index.ts +25 -0
- package/templates/features/jobs/files/src/apps/worker/index.ts +24 -0
- package/templates/features/jobs/files/src/core/jobs/commands/enqueue.ts +13 -0
- package/templates/features/jobs/files/src/core/jobs/define.ts +37 -0
- package/templates/features/jobs/files/src/core/jobs/lock.ts +17 -0
- package/templates/features/jobs/files/src/core/jobs/process.ts +19 -0
- package/templates/features/jobs/files/src/core/jobs/queue.ts +29 -0
- package/templates/features/jobs/files/src/core/jobs/redis.ts +17 -0
- package/templates/features/jobs/files/src/modules/example/consumers/send-welcome.ts +11 -0
- package/templates/features/jobs/files/src/modules/example/schedules/heartbeat.ts +9 -0
- package/templates/features/jobs/files/src/modules/example/tests/jobs.test.ts +29 -0
- package/templates/features/keycloak/files/keycloak/realm.json +83 -0
- package/templates/features/keycloak/files/src/core/auth/commands/keycloak-token.ts +32 -0
- package/templates/features/keycloak/files/src/core/auth/keycloak.ts +58 -0
- package/templates/features/keycloak/files/src/core/auth/principal.ts +27 -0
- package/templates/features/keycloak/files/tests/helpers/auth.ts +52 -0
- package/templates/features/keycloak/files+drizzle/src/core/auth/users.ts +18 -0
- package/templates/features/keycloak/files+drizzle+mysql/src/core/auth/db/user.schema.ts +10 -0
- package/templates/features/keycloak/files+drizzle+postgres/src/core/auth/db/user.schema.ts +10 -0
- package/templates/features/keycloak/files+prisma/prisma/models/user.prisma +10 -0
- package/templates/features/keycloak/files+prisma/src/core/auth/users.ts +13 -0
- package/templates/features/prisma/files/prisma/models/.gitkeep +0 -0
- package/templates/features/prisma/files/prisma.config.ts +10 -0
- package/templates/features/prisma/files/src/core/db/migrate.ts +13 -0
- package/templates/features/prisma/files+mysql/prisma/schema.prisma +11 -0
- package/templates/features/prisma/files+mysql/src/core/db/client.ts +23 -0
- package/templates/features/prisma/files+postgres/prisma/schema.prisma +11 -0
- package/templates/features/prisma/files+postgres/src/core/db/client.ts +19 -0
- package/templates/features/storage/files/src/core/storage/commands/check.ts +18 -0
- package/templates/features/storage/files/src/core/storage/index.ts +8 -0
- package/templates/features/storage/files/src/core/storage/local.ts +34 -0
- package/templates/features/storage/files/src/core/storage/s3.ts +66 -0
- package/templates/features/storage/files/src/core/storage/storage.ts +21 -0
- package/templates/features/storage/files/src/core/storage/tests/storage.test.ts +22 -0
- package/templates/generators/command/files/src/modules/__moduleKebab__/commands/__nameKebab__.ts +11 -0
- package/templates/generators/consumer/files/src/modules/__moduleKebab__/consumers/__nameKebab__.ts +14 -0
- package/templates/generators/consumer/files/src/modules/__moduleKebab__/tests/__nameKebab__.consumer.test.ts +12 -0
- package/templates/generators/module/files/src/modules/__moduleKebab__/routes/__moduleKebab__.routes.ts +20 -0
- package/templates/generators/module/files/src/modules/__moduleKebab__/services/__moduleKebab__.service.ts +4 -0
- package/templates/generators/module/files/src/modules/__moduleKebab__/tests/__moduleKebab__.test.ts +10 -0
- package/templates/generators/module/files/src/modules/__moduleKebab__/validators/__moduleKebab__.validators.ts +5 -0
- package/templates/generators/permission/files/src/modules/__moduleKebab__/permissions/__nameKebab__.permissions.ts +18 -0
- package/templates/generators/resource/files/src/modules/__moduleKebab__/routes/__nameKebab__.routes.ts +127 -0
- package/templates/generators/resource/files/src/modules/__moduleKebab__/tests/__nameKebab__.routes.test.ts +59 -0
- package/templates/generators/resource/files/src/modules/__moduleKebab__/validators/__nameKebab__.validators.ts +30 -0
- package/templates/generators/resource/files+authz/src/modules/__moduleKebab__/permissions/__nameKebab__.permissions.ts +21 -0
- package/templates/generators/resource/files+drizzle/src/modules/__moduleKebab__/services/__nameKebab__.service.ts +78 -0
- package/templates/generators/resource/files+drizzle+mysql/src/modules/__moduleKebab__/db/__nameKebab__.schema.ts +9 -0
- package/templates/generators/resource/files+drizzle+postgres/src/modules/__moduleKebab__/db/__nameKebab__.schema.ts +9 -0
- package/templates/generators/resource/files+prisma/prisma/models/__nameKebab__.prisma +9 -0
- package/templates/generators/resource/files+prisma/src/modules/__moduleKebab__/services/__nameKebab__.service.ts +73 -0
- package/templates/generators/route/files/src/modules/__moduleKebab__/routes/__nameKebab__.routes.ts +29 -0
- package/templates/generators/route/files/src/modules/__moduleKebab__/validators/__nameKebab__.validators.ts +9 -0
- package/templates/generators/route/files+authz/src/modules/__moduleKebab__/permissions/__nameKebab__.permissions.ts +13 -0
- package/templates/generators/schedule/files/src/modules/__moduleKebab__/schedules/__nameKebab__.ts +11 -0
- package/templates/generators/schema/files+drizzle+mysql/src/modules/__moduleKebab__/db/__nameKebab__.schema.ts +9 -0
- package/templates/generators/schema/files+drizzle+postgres/src/modules/__moduleKebab__/db/__nameKebab__.schema.ts +9 -0
- package/templates/generators/schema/files+prisma/prisma/models/__nameKebab__.prisma +9 -0
- package/templates/generators/service/files/src/modules/__moduleKebab__/services/__nameKebab__.service.ts +4 -0
- package/templates/generators/service/files/src/modules/__moduleKebab__/tests/__nameKebab__.service.test.ts +8 -0
- package/templates/versions.json +34 -0
- package/README.md +0 -3
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve, sep } from "node:path";
|
|
3
|
+
import { env } from "#core/config/env";
|
|
4
|
+
import { assertKey, type Storage } from "#core/storage/storage";
|
|
5
|
+
|
|
6
|
+
/** Files on disk; for development and tests. */
|
|
7
|
+
export function localStorage(root = env.STORAGE_LOCAL_DIR): Storage {
|
|
8
|
+
const base = resolve(root);
|
|
9
|
+
const path = (key: string) => {
|
|
10
|
+
const full = resolve(base, assertKey(key));
|
|
11
|
+
if (!full.startsWith(base + sep)) throw new Error(`Invalid storage key "${key}"`);
|
|
12
|
+
return full;
|
|
13
|
+
};
|
|
14
|
+
return {
|
|
15
|
+
async put(key, body) {
|
|
16
|
+
await mkdir(dirname(path(key)), { recursive: true });
|
|
17
|
+
await writeFile(path(key), body);
|
|
18
|
+
},
|
|
19
|
+
async get(key) {
|
|
20
|
+
try {
|
|
21
|
+
return new Uint8Array(await readFile(path(key)));
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
async delete(key) {
|
|
28
|
+
await rm(path(key), { force: true });
|
|
29
|
+
},
|
|
30
|
+
async url(key) {
|
|
31
|
+
return `file://${path(key)}`;
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CreateBucketCommand,
|
|
3
|
+
DeleteObjectCommand,
|
|
4
|
+
GetObjectCommand,
|
|
5
|
+
HeadBucketCommand,
|
|
6
|
+
NoSuchKey,
|
|
7
|
+
PutObjectCommand,
|
|
8
|
+
S3Client,
|
|
9
|
+
} from "@aws-sdk/client-s3";
|
|
10
|
+
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
11
|
+
import { env } from "#core/config/env";
|
|
12
|
+
import { assertKey, type Storage } from "#core/storage/storage";
|
|
13
|
+
|
|
14
|
+
export const s3 = new S3Client({
|
|
15
|
+
region: env.S3_REGION,
|
|
16
|
+
...(env.S3_ENDPOINT ? { endpoint: env.S3_ENDPOINT } : {}),
|
|
17
|
+
forcePathStyle: env.S3_FORCE_PATH_STYLE,
|
|
18
|
+
...(env.S3_ACCESS_KEY_ID && env.S3_SECRET_ACCESS_KEY
|
|
19
|
+
? { credentials: { accessKeyId: env.S3_ACCESS_KEY_ID, secretAccessKey: env.S3_SECRET_ACCESS_KEY } }
|
|
20
|
+
: {}),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/** Any S3-compatible store (AWS S3, R2, RustFS locally). */
|
|
24
|
+
export function s3Storage(bucket = env.S3_BUCKET): Storage {
|
|
25
|
+
let bucketReady: Promise<boolean> | undefined;
|
|
26
|
+
return {
|
|
27
|
+
async put(key, body, contentType) {
|
|
28
|
+
// local development: create the bucket on first write
|
|
29
|
+
if (env.NODE_ENV === "development") {
|
|
30
|
+
bucketReady ??= ensureBucket(bucket);
|
|
31
|
+
await bucketReady;
|
|
32
|
+
}
|
|
33
|
+
await s3.send(
|
|
34
|
+
new PutObjectCommand({ Bucket: bucket, Key: assertKey(key), Body: body, ContentType: contentType }),
|
|
35
|
+
);
|
|
36
|
+
},
|
|
37
|
+
async get(key) {
|
|
38
|
+
try {
|
|
39
|
+
const res = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: assertKey(key) }));
|
|
40
|
+
return res.Body ? await res.Body.transformToByteArray() : null;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (error instanceof NoSuchKey) return null;
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
async delete(key) {
|
|
47
|
+
await s3.send(new DeleteObjectCommand({ Bucket: bucket, Key: assertKey(key) }));
|
|
48
|
+
},
|
|
49
|
+
async url(key, expiresInSeconds = 900) {
|
|
50
|
+
return getSignedUrl(s3, new GetObjectCommand({ Bucket: bucket, Key: assertKey(key) }), {
|
|
51
|
+
expiresIn: expiresInSeconds,
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Creates the bucket when it does not exist (local development). */
|
|
58
|
+
export async function ensureBucket(bucket = env.S3_BUCKET): Promise<boolean> {
|
|
59
|
+
try {
|
|
60
|
+
await s3.send(new HeadBucketCommand({ Bucket: bucket }));
|
|
61
|
+
return false;
|
|
62
|
+
} catch {
|
|
63
|
+
await s3.send(new CreateBucketCommand({ Bucket: bucket }));
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Object storage used by modules; pick the driver with STORAGE_DRIVER. */
|
|
2
|
+
export interface Storage {
|
|
3
|
+
put(key: string, body: Uint8Array | string, contentType?: string): Promise<void>;
|
|
4
|
+
get(key: string): Promise<Uint8Array | null>;
|
|
5
|
+
delete(key: string): Promise<void>;
|
|
6
|
+
/** a URL clients can fetch the object from (signed for S3) */
|
|
7
|
+
url(key: string, expiresInSeconds?: number): Promise<string>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Keys are relative, forward-slash paths without "..", backslashes, NUL bytes or drive prefixes. */
|
|
11
|
+
export function assertKey(key: string): string {
|
|
12
|
+
const invalid =
|
|
13
|
+
!key ||
|
|
14
|
+
key.startsWith("/") ||
|
|
15
|
+
key.includes("\\") ||
|
|
16
|
+
key.includes("\0") ||
|
|
17
|
+
/^[A-Za-z]:/.test(key) ||
|
|
18
|
+
key.split("/").some((segment) => segment === ".." || segment === ".");
|
|
19
|
+
if (invalid) throw new Error(`Invalid storage key "${key}"`);
|
|
20
|
+
return key;
|
|
21
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { mkdtemp } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { localStorage } from "#core/storage/local";
|
|
6
|
+
|
|
7
|
+
describe("local storage", () => {
|
|
8
|
+
it("round-trips an object", async () => {
|
|
9
|
+
const store = localStorage(await mkdtemp(join(tmpdir(), "storage-")));
|
|
10
|
+
await store.put("a/b.txt", "hello");
|
|
11
|
+
expect(new TextDecoder().decode((await store.get("a/b.txt")) ?? new Uint8Array())).toBe("hello");
|
|
12
|
+
await store.delete("a/b.txt");
|
|
13
|
+
expect(await store.get("a/b.txt")).toBeNull();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("rejects keys that escape the root", async () => {
|
|
17
|
+
const store = localStorage(await mkdtemp(join(tmpdir(), "storage-")));
|
|
18
|
+
for (const key of ["../x", "a/../../x", "a\\..\\x", "/etc/passwd", "C:/x", "a/\0b"]) {
|
|
19
|
+
await expect(store.put(key, "no")).rejects.toThrow(/Invalid storage key/);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
});
|
package/templates/generators/command/files/src/modules/__moduleKebab__/commands/__nameKebab__.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineCommand, flag } from "#core/commands/define";
|
|
3
|
+
|
|
4
|
+
export default defineCommand({
|
|
5
|
+
name: "__moduleKebab__:__nameKebab__",
|
|
6
|
+
description: "__nameTitle__",
|
|
7
|
+
args: z.object({ dryRun: flag() }),
|
|
8
|
+
async run({ dryRun }, { logger }) {
|
|
9
|
+
logger.info({ dryRun }, "__moduleKebab__:__nameKebab__ ran");
|
|
10
|
+
},
|
|
11
|
+
});
|
package/templates/generators/consumer/files/src/modules/__moduleKebab__/consumers/__nameKebab__.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineConsumer } from "#core/jobs/define";
|
|
3
|
+
|
|
4
|
+
export const __nameCamel__Payload = z.object({ id: z.string().min(1) });
|
|
5
|
+
|
|
6
|
+
export default defineConsumer({
|
|
7
|
+
topic: "__moduleKebab__.__nameKebab__",
|
|
8
|
+
description: "__nameTitle__",
|
|
9
|
+
schema: __nameCamel__Payload,
|
|
10
|
+
async handle({ id }, { logger }) {
|
|
11
|
+
// must be idempotent: a job can run more than once
|
|
12
|
+
logger.info({ id }, "__moduleKebab__.__nameKebab__ handled");
|
|
13
|
+
},
|
|
14
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { processJob } from "#core/jobs/process";
|
|
3
|
+
import { logger } from "#core/logging/logger";
|
|
4
|
+
import { consumers } from "#generated/consumers";
|
|
5
|
+
|
|
6
|
+
describe("__moduleKebab__.__nameKebab__", () => {
|
|
7
|
+
it("handles a valid payload", async () => {
|
|
8
|
+
await expect(
|
|
9
|
+
processJob(consumers, "__moduleKebab__.__nameKebab__", { id: "1" }, { logger, jobId: "t", attempt: 1 }),
|
|
10
|
+
).resolves.toBeUndefined();
|
|
11
|
+
});
|
|
12
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createRoute } from "@hono/zod-openapi";
|
|
2
|
+
import { jsonContent, success, successSchema } from "#core/http/response";
|
|
3
|
+
import { createRouter, defineRoutes } from "#core/http/routes";
|
|
4
|
+
import { __moduleCamel__Status } from "#modules/__moduleKebab__/services/__moduleKebab__.service";
|
|
5
|
+
import { __moduleCamel__StatusSchema } from "#modules/__moduleKebab__/validators/__moduleKebab__.validators";
|
|
6
|
+
|
|
7
|
+
const router = createRouter();
|
|
8
|
+
|
|
9
|
+
router.openapi(
|
|
10
|
+
createRoute({
|
|
11
|
+
method: "get",
|
|
12
|
+
path: "/status",
|
|
13
|
+
tags: ["__modulePascal__"],
|
|
14
|
+
summary: "__moduleTitle__ module status",
|
|
15
|
+
responses: { 200: jsonContent(successSchema(__moduleCamel__StatusSchema), "Status") },
|
|
16
|
+
}),
|
|
17
|
+
(c) => c.json(success(__moduleCamel__Status()), 200),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
export default defineRoutes({ app: "api", basePath: "/__moduleKebab__", router });
|
package/templates/generators/module/files/src/modules/__moduleKebab__/tests/__moduleKebab__.test.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { api } from "#tests/helpers/api";
|
|
3
|
+
|
|
4
|
+
describe("__moduleKebab__", () => {
|
|
5
|
+
it("reports status", async () => {
|
|
6
|
+
const res = await api.get("/api/__moduleKebab__/status");
|
|
7
|
+
expect(res.status).toBe(200);
|
|
8
|
+
expect(res.body).toEqual({ success: true, data: { module: "__moduleKebab__", status: "ok" } });
|
|
9
|
+
});
|
|
10
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { definePermissions } from "#core/authz/catalog";
|
|
2
|
+
|
|
3
|
+
export default definePermissions({
|
|
4
|
+
resources: {
|
|
5
|
+
"__nameKebab__": {
|
|
6
|
+
// scoped actions are checked at own | team | org; use resolveScope() in the service
|
|
7
|
+
read: { scoped: true },
|
|
8
|
+
manage: { scoped: false },
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
roleGrants: {
|
|
12
|
+
admin: [
|
|
13
|
+
["__nameKebab__", "read", "org"],
|
|
14
|
+
["__nameKebab__", "manage"],
|
|
15
|
+
],
|
|
16
|
+
member: [["__nameKebab__", "read", "own"]],
|
|
17
|
+
},
|
|
18
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { createRoute } from "@hono/zod-openapi";
|
|
2
|
+
import {
|
|
3
|
+
errorResponses,
|
|
4
|
+
jsonBody,
|
|
5
|
+
jsonContent,
|
|
6
|
+
paginated,
|
|
7
|
+
paginatedSchema,
|
|
8
|
+
paginationQuerySchema,
|
|
9
|
+
success,
|
|
10
|
+
successSchema,
|
|
11
|
+
} from "#core/http/response";
|
|
12
|
+
import { createRouter, defineRoutes } from "#core/http/routes";
|
|
13
|
+
// tatara:slot imports
|
|
14
|
+
import {
|
|
15
|
+
create__namePascal__,
|
|
16
|
+
get__namePascal__,
|
|
17
|
+
list__namePluralPascal__,
|
|
18
|
+
remove__namePascal__,
|
|
19
|
+
update__namePascal__,
|
|
20
|
+
} from "#modules/__moduleKebab__/services/__nameKebab__.service";
|
|
21
|
+
import {
|
|
22
|
+
__nameCamel__ParamsSchema,
|
|
23
|
+
__nameCamel__Schema,
|
|
24
|
+
create__namePascal__Schema,
|
|
25
|
+
update__namePascal__Schema,
|
|
26
|
+
} from "#modules/__moduleKebab__/validators/__nameKebab__.validators";
|
|
27
|
+
|
|
28
|
+
const router = createRouter();
|
|
29
|
+
const tags = ["__namePluralPascal__"];
|
|
30
|
+
|
|
31
|
+
router.openapi(
|
|
32
|
+
createRoute({
|
|
33
|
+
method: "get",
|
|
34
|
+
path: "/",
|
|
35
|
+
tags,
|
|
36
|
+
summary: "List __namePluralKebab__",
|
|
37
|
+
// tatara:slot guard-list
|
|
38
|
+
request: { query: paginationQuerySchema },
|
|
39
|
+
responses: {
|
|
40
|
+
200: jsonContent(paginatedSchema(__nameCamel__Schema), "A page of __namePluralKebab__"),
|
|
41
|
+
...errorResponses(400),
|
|
42
|
+
// tatara:slot auth-errors
|
|
43
|
+
},
|
|
44
|
+
}),
|
|
45
|
+
async (c) => {
|
|
46
|
+
const query = c.req.valid("query");
|
|
47
|
+
const { items, total } = await list__namePluralPascal__(query);
|
|
48
|
+
return c.json(paginated(items, query.page, query.pageSize, total), 200);
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
router.openapi(
|
|
53
|
+
createRoute({
|
|
54
|
+
method: "get",
|
|
55
|
+
path: "/{id}",
|
|
56
|
+
tags,
|
|
57
|
+
summary: "Get a __nameKebab__",
|
|
58
|
+
// tatara:slot guard-read
|
|
59
|
+
request: { params: __nameCamel__ParamsSchema },
|
|
60
|
+
responses: {
|
|
61
|
+
200: jsonContent(successSchema(__nameCamel__Schema), "The __nameKebab__"),
|
|
62
|
+
...errorResponses(400, 404),
|
|
63
|
+
// tatara:slot auth-errors
|
|
64
|
+
},
|
|
65
|
+
}),
|
|
66
|
+
async (c) => c.json(success(await get__namePascal__(c.req.valid("param").id)), 200),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
router.openapi(
|
|
70
|
+
createRoute({
|
|
71
|
+
method: "post",
|
|
72
|
+
path: "/",
|
|
73
|
+
tags,
|
|
74
|
+
summary: "Create a __nameKebab__",
|
|
75
|
+
// tatara:slot guard-create
|
|
76
|
+
request: { body: jsonBody(create__namePascal__Schema, "The new __nameKebab__") },
|
|
77
|
+
responses: {
|
|
78
|
+
201: jsonContent(successSchema(__nameCamel__Schema), "Created"),
|
|
79
|
+
...errorResponses(400),
|
|
80
|
+
// tatara:slot auth-errors
|
|
81
|
+
},
|
|
82
|
+
}),
|
|
83
|
+
async (c) => c.json(success(await create__namePascal__(c.req.valid("json"))), 201),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
router.openapi(
|
|
87
|
+
createRoute({
|
|
88
|
+
method: "patch",
|
|
89
|
+
path: "/{id}",
|
|
90
|
+
tags,
|
|
91
|
+
summary: "Update a __nameKebab__",
|
|
92
|
+
// tatara:slot guard-update
|
|
93
|
+
request: {
|
|
94
|
+
params: __nameCamel__ParamsSchema,
|
|
95
|
+
body: jsonBody(update__namePascal__Schema, "Fields to change"),
|
|
96
|
+
},
|
|
97
|
+
responses: {
|
|
98
|
+
200: jsonContent(successSchema(__nameCamel__Schema), "Updated"),
|
|
99
|
+
...errorResponses(400, 404),
|
|
100
|
+
// tatara:slot auth-errors
|
|
101
|
+
},
|
|
102
|
+
}),
|
|
103
|
+
async (c) =>
|
|
104
|
+
c.json(success(await update__namePascal__(c.req.valid("param").id, c.req.valid("json"))), 200),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
router.openapi(
|
|
108
|
+
createRoute({
|
|
109
|
+
method: "delete",
|
|
110
|
+
path: "/{id}",
|
|
111
|
+
tags,
|
|
112
|
+
summary: "Delete a __nameKebab__",
|
|
113
|
+
// tatara:slot guard-delete
|
|
114
|
+
request: { params: __nameCamel__ParamsSchema },
|
|
115
|
+
responses: {
|
|
116
|
+
204: { description: "Deleted" },
|
|
117
|
+
...errorResponses(400, 404),
|
|
118
|
+
// tatara:slot auth-errors
|
|
119
|
+
},
|
|
120
|
+
}),
|
|
121
|
+
async (c) => {
|
|
122
|
+
await remove__namePascal__(c.req.valid("param").id);
|
|
123
|
+
return c.body(null, 204);
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
export default defineRoutes({ app: "api", basePath: "/__namePluralKebab__", router });
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { api } from "#tests/helpers/api";
|
|
3
|
+
import { resetDatabase } from "#tests/helpers/db";
|
|
4
|
+
// tatara:slot test-imports
|
|
5
|
+
|
|
6
|
+
const path = "/api/__namePluralKebab__";
|
|
7
|
+
const headers: Record<string, string> = {};
|
|
8
|
+
|
|
9
|
+
interface __namePascal__Body {
|
|
10
|
+
data: { id: string; name: string; description: string | null };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
await resetDatabase();
|
|
15
|
+
// tatara:slot test-auth
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("__namePluralKebab__", () => {
|
|
19
|
+
it("creates, reads, updates, lists and deletes", async () => {
|
|
20
|
+
const created = await api.post<__namePascal__Body>(path, { name: "First" }, { headers });
|
|
21
|
+
expect(created.status).toBe(201);
|
|
22
|
+
const { id } = created.body.data;
|
|
23
|
+
expect(created.body.data).toMatchObject({ name: "First", description: null });
|
|
24
|
+
|
|
25
|
+
const read = await api.get<__namePascal__Body>(`${path}/${id}`, { headers });
|
|
26
|
+
expect(read.status).toBe(200);
|
|
27
|
+
expect(read.body.data.id).toBe(id);
|
|
28
|
+
|
|
29
|
+
const updated = await api.patch<__namePascal__Body>(
|
|
30
|
+
`${path}/${id}`,
|
|
31
|
+
{ description: "Changed" },
|
|
32
|
+
{ headers },
|
|
33
|
+
);
|
|
34
|
+
expect(updated.status).toBe(200);
|
|
35
|
+
expect(updated.body.data).toMatchObject({ name: "First", description: "Changed" });
|
|
36
|
+
|
|
37
|
+
await api.post(path, { name: "Second" }, { headers });
|
|
38
|
+
const list = await api.get<{ data: { name: string }[]; meta: { total: number } }>(
|
|
39
|
+
`${path}?pageSize=10`,
|
|
40
|
+
{ headers },
|
|
41
|
+
);
|
|
42
|
+
expect(list.status).toBe(200);
|
|
43
|
+
expect(list.body.meta.total).toBe(2);
|
|
44
|
+
expect(list.body.data.map((item) => item.name)).toEqual(["Second", "First"]);
|
|
45
|
+
|
|
46
|
+
const removed = await api.delete(`${path}/${id}`, { headers });
|
|
47
|
+
expect(removed.status).toBe(204);
|
|
48
|
+
const gone = await api.get(`${path}/${id}`, { headers });
|
|
49
|
+
expect(gone.status).toBe(404);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("rejects invalid input", async () => {
|
|
53
|
+
const res = await api.post(path, { name: "" }, { headers });
|
|
54
|
+
expect(res.status).toBe(400);
|
|
55
|
+
expect(res.body).toMatchObject({ success: false, code: "VALIDATION_ERROR" });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// tatara:slot test-cases
|
|
59
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from "@hono/zod-openapi";
|
|
2
|
+
|
|
3
|
+
export const __nameCamel__Schema = z
|
|
4
|
+
.object({
|
|
5
|
+
id: z.string().openapi({ example: "01920000-0000-7000-8000-000000000000" }),
|
|
6
|
+
name: z.string().openapi({ example: "My __nameTitle__" }),
|
|
7
|
+
description: z.string().nullable(),
|
|
8
|
+
createdAt: z.string().openapi({ format: "date-time" }),
|
|
9
|
+
updatedAt: z.string().openapi({ format: "date-time" }),
|
|
10
|
+
})
|
|
11
|
+
.openapi("__namePascal__");
|
|
12
|
+
|
|
13
|
+
export const create__namePascal__Schema = z
|
|
14
|
+
.object({
|
|
15
|
+
name: z.string().trim().min(1).max(200),
|
|
16
|
+
description: z.string().max(5000).nullable().optional(),
|
|
17
|
+
})
|
|
18
|
+
.openapi("Create__namePascal__");
|
|
19
|
+
|
|
20
|
+
export const update__namePascal__Schema = create__namePascal__Schema
|
|
21
|
+
.partial()
|
|
22
|
+
.openapi("Update__namePascal__");
|
|
23
|
+
|
|
24
|
+
export const __nameCamel__ParamsSchema = z.object({
|
|
25
|
+
id: z.string().min(1).openapi({ param: { name: "id", in: "path" } }),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export type __namePascal__ = z.infer<typeof __nameCamel__Schema>;
|
|
29
|
+
export type Create__namePascal__Input = z.infer<typeof create__namePascal__Schema>;
|
|
30
|
+
export type Update__namePascal__Input = z.infer<typeof update__namePascal__Schema>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { definePermissions } from "#core/authz/catalog";
|
|
2
|
+
|
|
3
|
+
export default definePermissions({
|
|
4
|
+
resources: {
|
|
5
|
+
"__nameKebab__": {
|
|
6
|
+
read: { scoped: false },
|
|
7
|
+
create: { scoped: false },
|
|
8
|
+
update: { scoped: false },
|
|
9
|
+
delete: { scoped: false },
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
roleGrants: {
|
|
13
|
+
admin: [
|
|
14
|
+
["__nameKebab__", "read"],
|
|
15
|
+
["__nameKebab__", "create"],
|
|
16
|
+
["__nameKebab__", "update"],
|
|
17
|
+
["__nameKebab__", "delete"],
|
|
18
|
+
],
|
|
19
|
+
member: [["__nameKebab__", "read"]],
|
|
20
|
+
},
|
|
21
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { count, desc, eq } from "drizzle-orm";
|
|
2
|
+
import { db, type Executor } from "#core/db/client";
|
|
3
|
+
import { newId } from "#core/db/id";
|
|
4
|
+
import { NotFoundError } from "#core/http/errors";
|
|
5
|
+
import type { PaginationQuery } from "#core/http/response";
|
|
6
|
+
import { __namePluralCamel__ } from "#modules/__moduleKebab__/db/__nameKebab__.schema";
|
|
7
|
+
import type {
|
|
8
|
+
Create__namePascal__Input,
|
|
9
|
+
__namePascal__,
|
|
10
|
+
Update__namePascal__Input,
|
|
11
|
+
} from "#modules/__moduleKebab__/validators/__nameKebab__.validators";
|
|
12
|
+
|
|
13
|
+
type Row = typeof __namePluralCamel__.$inferSelect;
|
|
14
|
+
|
|
15
|
+
function to__namePascal__(row: Row): __namePascal__ {
|
|
16
|
+
return {
|
|
17
|
+
id: row.id,
|
|
18
|
+
name: row.name,
|
|
19
|
+
description: row.description,
|
|
20
|
+
createdAt: row.createdAt.toISOString(),
|
|
21
|
+
updatedAt: row.updatedAt.toISOString(),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function list__namePluralPascal__(
|
|
26
|
+
{ page, pageSize }: PaginationQuery,
|
|
27
|
+
executor: Executor = db,
|
|
28
|
+
) {
|
|
29
|
+
const [rows, totals] = await Promise.all([
|
|
30
|
+
executor
|
|
31
|
+
.select()
|
|
32
|
+
.from(__namePluralCamel__)
|
|
33
|
+
.orderBy(desc(__namePluralCamel__.id))
|
|
34
|
+
.limit(pageSize)
|
|
35
|
+
.offset((page - 1) * pageSize),
|
|
36
|
+
executor.select({ total: count() }).from(__namePluralCamel__),
|
|
37
|
+
]);
|
|
38
|
+
return { items: rows.map(to__namePascal__), total: totals[0]?.total ?? 0 };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function get__namePascal__(id: string, executor: Executor = db): Promise<__namePascal__> {
|
|
42
|
+
const [row] = await executor
|
|
43
|
+
.select()
|
|
44
|
+
.from(__namePluralCamel__)
|
|
45
|
+
.where(eq(__namePluralCamel__.id, id))
|
|
46
|
+
.limit(1);
|
|
47
|
+
if (!row) throw new NotFoundError("__nameTitle__");
|
|
48
|
+
return to__namePascal__(row);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function create__namePascal__(
|
|
52
|
+
input: Create__namePascal__Input,
|
|
53
|
+
executor: Executor = db,
|
|
54
|
+
): Promise<__namePascal__> {
|
|
55
|
+
const id = newId();
|
|
56
|
+
await executor
|
|
57
|
+
.insert(__namePluralCamel__)
|
|
58
|
+
.values({ id, name: input.name, description: input.description ?? null });
|
|
59
|
+
return get__namePascal__(id, executor);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function update__namePascal__(
|
|
63
|
+
id: string,
|
|
64
|
+
input: Update__namePascal__Input,
|
|
65
|
+
executor: Executor = db,
|
|
66
|
+
): Promise<__namePascal__> {
|
|
67
|
+
await get__namePascal__(id, executor);
|
|
68
|
+
await executor
|
|
69
|
+
.update(__namePluralCamel__)
|
|
70
|
+
.set({ ...input, updatedAt: new Date() })
|
|
71
|
+
.where(eq(__namePluralCamel__.id, id));
|
|
72
|
+
return get__namePascal__(id, executor);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function remove__namePascal__(id: string, executor: Executor = db): Promise<void> {
|
|
76
|
+
await get__namePascal__(id, executor);
|
|
77
|
+
await executor.delete(__namePluralCamel__).where(eq(__namePluralCamel__.id, id));
|
|
78
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { mysqlTable, text, timestamp, varchar } from "drizzle-orm/mysql-core";
|
|
2
|
+
|
|
3
|
+
export const __namePluralCamel__ = mysqlTable("__namePluralSnake__", {
|
|
4
|
+
id: varchar({ length: 36 }).primaryKey(),
|
|
5
|
+
name: varchar({ length: 200 }).notNull(),
|
|
6
|
+
description: text(),
|
|
7
|
+
createdAt: timestamp({ fsp: 3, mode: "date" }).notNull().defaultNow(),
|
|
8
|
+
updatedAt: timestamp({ fsp: 3, mode: "date" }).notNull().defaultNow(),
|
|
9
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { pgTable, text, timestamp, uuid, varchar } from "drizzle-orm/pg-core";
|
|
2
|
+
|
|
3
|
+
export const __namePluralCamel__ = pgTable("__namePluralSnake__", {
|
|
4
|
+
id: uuid().primaryKey(),
|
|
5
|
+
name: varchar({ length: 200 }).notNull(),
|
|
6
|
+
description: text(),
|
|
7
|
+
createdAt: timestamp({ withTimezone: true, mode: "date" }).notNull().defaultNow(),
|
|
8
|
+
updatedAt: timestamp({ withTimezone: true, mode: "date" }).notNull().defaultNow(),
|
|
9
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
model __namePascal__ {
|
|
2
|
+
id String @id @db.VarChar(36)
|
|
3
|
+
name String @db.VarChar(200)
|
|
4
|
+
description String? @db.Text
|
|
5
|
+
createdAt DateTime @default(now()) @map("created_at")
|
|
6
|
+
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
|
7
|
+
|
|
8
|
+
@@map("__namePluralSnake__")
|
|
9
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { db, type Executor } from "#core/db/client";
|
|
2
|
+
import { newId } from "#core/db/id";
|
|
3
|
+
import { NotFoundError } from "#core/http/errors";
|
|
4
|
+
import type { PaginationQuery } from "#core/http/response";
|
|
5
|
+
import type {
|
|
6
|
+
Create__namePascal__Input,
|
|
7
|
+
__namePascal__,
|
|
8
|
+
Update__namePascal__Input,
|
|
9
|
+
} from "#modules/__moduleKebab__/validators/__nameKebab__.validators";
|
|
10
|
+
|
|
11
|
+
interface Row {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
description: string | null;
|
|
15
|
+
createdAt: Date;
|
|
16
|
+
updatedAt: Date;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function to__namePascal__(row: Row): __namePascal__ {
|
|
20
|
+
return {
|
|
21
|
+
id: row.id,
|
|
22
|
+
name: row.name,
|
|
23
|
+
description: row.description,
|
|
24
|
+
createdAt: row.createdAt.toISOString(),
|
|
25
|
+
updatedAt: row.updatedAt.toISOString(),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function list__namePluralPascal__(
|
|
30
|
+
{ page, pageSize }: PaginationQuery,
|
|
31
|
+
executor: Executor = db,
|
|
32
|
+
) {
|
|
33
|
+
const [rows, total] = await Promise.all([
|
|
34
|
+
executor.__nameCamel__.findMany({
|
|
35
|
+
orderBy: { id: "desc" },
|
|
36
|
+
skip: (page - 1) * pageSize,
|
|
37
|
+
take: pageSize,
|
|
38
|
+
}),
|
|
39
|
+
executor.__nameCamel__.count(),
|
|
40
|
+
]);
|
|
41
|
+
return { items: rows.map(to__namePascal__), total };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function get__namePascal__(id: string, executor: Executor = db): Promise<__namePascal__> {
|
|
45
|
+
const row = await executor.__nameCamel__.findUnique({ where: { id } });
|
|
46
|
+
if (!row) throw new NotFoundError("__nameTitle__");
|
|
47
|
+
return to__namePascal__(row);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function create__namePascal__(
|
|
51
|
+
input: Create__namePascal__Input,
|
|
52
|
+
executor: Executor = db,
|
|
53
|
+
): Promise<__namePascal__> {
|
|
54
|
+
const row = await executor.__nameCamel__.create({
|
|
55
|
+
data: { id: newId(), name: input.name, description: input.description ?? null },
|
|
56
|
+
});
|
|
57
|
+
return to__namePascal__(row);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function update__namePascal__(
|
|
61
|
+
id: string,
|
|
62
|
+
input: Update__namePascal__Input,
|
|
63
|
+
executor: Executor = db,
|
|
64
|
+
): Promise<__namePascal__> {
|
|
65
|
+
await get__namePascal__(id, executor);
|
|
66
|
+
const row = await executor.__nameCamel__.update({ where: { id }, data: input });
|
|
67
|
+
return to__namePascal__(row);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function remove__namePascal__(id: string, executor: Executor = db): Promise<void> {
|
|
71
|
+
await get__namePascal__(id, executor);
|
|
72
|
+
await executor.__nameCamel__.delete({ where: { id } });
|
|
73
|
+
}
|