schematic-pg 0.1.0
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/README.md +1019 -0
- package/dist/api/auth/errors.d.ts +6 -0
- package/dist/api/auth/errors.js +12 -0
- package/dist/api/auth/jwt-resolver.d.ts +7 -0
- package/dist/api/auth/jwt-resolver.js +59 -0
- package/dist/api/auth/middleware.d.ts +4 -0
- package/dist/api/auth/middleware.js +10 -0
- package/dist/api/auth/policy.d.ts +7 -0
- package/dist/api/auth/policy.js +95 -0
- package/dist/api/auth/template.d.ts +2 -0
- package/dist/api/auth/template.js +24 -0
- package/dist/api/auth/types.d.ts +12 -0
- package/dist/api/auth/types.js +1 -0
- package/dist/api/middleware/db.d.ts +8 -0
- package/dist/api/middleware/db.js +12 -0
- package/dist/api/middleware/errors.d.ts +5 -0
- package/dist/api/middleware/errors.js +27 -0
- package/dist/api/middleware/validate.d.ts +23 -0
- package/dist/api/middleware/validate.js +13 -0
- package/dist/api/types.d.ts +8 -0
- package/dist/api/types.js +1 -0
- package/dist/api/utils/route-naming.d.ts +3 -0
- package/dist/api/utils/route-naming.js +25 -0
- package/dist/api-generator/app-generator.d.ts +11 -0
- package/dist/api-generator/app-generator.js +79 -0
- package/dist/api-generator/custom-route-scanner.d.ts +6 -0
- package/dist/api-generator/custom-route-scanner.js +42 -0
- package/dist/api-generator/generate-api-cli.d.ts +1 -0
- package/dist/api-generator/generate-api-cli.js +28 -0
- package/dist/api-generator/index.d.ts +11 -0
- package/dist/api-generator/index.js +15 -0
- package/dist/api-generator/policy-generator.d.ts +9 -0
- package/dist/api-generator/policy-generator.js +33 -0
- package/dist/api-generator/route-generator.d.ts +20 -0
- package/dist/api-generator/route-generator.js +198 -0
- package/dist/api-generator/utils/policy.d.ts +18 -0
- package/dist/api-generator/utils/policy.js +72 -0
- package/dist/api-generator/zod-schema-generator.d.ts +16 -0
- package/dist/api-generator/zod-schema-generator.js +145 -0
- package/dist/cli/db.d.ts +4 -0
- package/dist/cli/db.js +144 -0
- package/dist/cli/dev.d.ts +1 -0
- package/dist/cli/dev.js +10 -0
- package/dist/cli/generate.d.ts +4 -0
- package/dist/cli/generate.js +50 -0
- package/dist/cli/init.d.ts +1 -0
- package/dist/cli/init.js +57 -0
- package/dist/cli/paths.d.ts +5 -0
- package/dist/cli/paths.js +10 -0
- package/dist/cli/templates.d.ts +6 -0
- package/dist/cli/templates.js +85 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +81 -0
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- package/dist/db/bootstrap-cli.d.ts +1 -0
- package/dist/db/bootstrap-cli.js +17 -0
- package/dist/db/bootstrap.d.ts +3 -0
- package/dist/db/bootstrap.js +16 -0
- package/dist/db/cli.d.ts +1 -0
- package/dist/db/cli.js +20 -0
- package/dist/db/client.d.ts +8 -0
- package/dist/db/client.js +23 -0
- package/dist/db/config.d.ts +1 -0
- package/dist/db/config.js +10 -0
- package/dist/db/db-client-generator.d.ts +13 -0
- package/dist/db/db-client-generator.js +70 -0
- package/dist/db/diff-cli.d.ts +1 -0
- package/dist/db/diff-cli.js +46 -0
- package/dist/db/diff.d.ts +9 -0
- package/dist/db/diff.js +30 -0
- package/dist/db/errors.d.ts +34 -0
- package/dist/db/errors.js +88 -0
- package/dist/db/generate-client-cli.d.ts +1 -0
- package/dist/db/generate-client-cli.js +21 -0
- package/dist/db/index.d.ts +19 -0
- package/dist/db/index.js +17 -0
- package/dist/db/load-env.d.ts +3 -0
- package/dist/db/load-env.js +19 -0
- package/dist/db/migrate-cli.d.ts +1 -0
- package/dist/db/migrate-cli.js +88 -0
- package/dist/db/migrate.d.ts +3 -0
- package/dist/db/migrate.js +32 -0
- package/dist/db/migrations.d.ts +17 -0
- package/dist/db/migrations.js +81 -0
- package/dist/db/model-client.d.ts +36 -0
- package/dist/db/model-client.js +83 -0
- package/dist/db/model-meta.d.ts +36 -0
- package/dist/db/model-meta.js +57 -0
- package/dist/db/query-builder.d.ts +33 -0
- package/dist/db/query-builder.js +97 -0
- package/dist/db/reset-database.d.ts +4 -0
- package/dist/db/reset-database.js +9 -0
- package/dist/db/row-mapper.d.ts +3 -0
- package/dist/db/row-mapper.js +41 -0
- package/dist/db/schema-state.d.ts +7 -0
- package/dist/db/schema-state.js +32 -0
- package/dist/db/type-generator.d.ts +19 -0
- package/dist/db/type-generator.js +136 -0
- package/dist/db/utils/naming.d.ts +6 -0
- package/dist/db/utils/naming.js +23 -0
- package/dist/db/where-translator.d.ts +20 -0
- package/dist/db/where-translator.js +141 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/routes/health.d.ts +4 -0
- package/dist/routes/health.js +4 -0
- package/dist/schema-dsl/ast.d.ts +108 -0
- package/dist/schema-dsl/ast.js +1 -0
- package/dist/schema-dsl/cli.d.ts +1 -0
- package/dist/schema-dsl/cli.js +25 -0
- package/dist/schema-dsl/index.d.ts +8 -0
- package/dist/schema-dsl/index.js +16 -0
- package/dist/schema-dsl/inspect.d.ts +1 -0
- package/dist/schema-dsl/inspect.js +9 -0
- package/dist/schema-dsl/lexer.d.ts +31 -0
- package/dist/schema-dsl/lexer.js +216 -0
- package/dist/schema-dsl/parser.d.ts +49 -0
- package/dist/schema-dsl/parser.js +372 -0
- package/dist/schema-dsl/tokens.d.ts +30 -0
- package/dist/schema-dsl/tokens.js +35 -0
- package/dist/sql-generator/cli.d.ts +1 -0
- package/dist/sql-generator/cli.js +7 -0
- package/dist/sql-generator/generators/drop-tables.d.ts +2 -0
- package/dist/sql-generator/generators/drop-tables.js +8 -0
- package/dist/sql-generator/generators/enums.d.ts +4 -0
- package/dist/sql-generator/generators/enums.js +16 -0
- package/dist/sql-generator/generators/extensions.d.ts +4 -0
- package/dist/sql-generator/generators/extensions.js +11 -0
- package/dist/sql-generator/generators/foreign-keys.d.ts +4 -0
- package/dist/sql-generator/generators/foreign-keys.js +23 -0
- package/dist/sql-generator/generators/indexes.d.ts +13 -0
- package/dist/sql-generator/generators/indexes.js +39 -0
- package/dist/sql-generator/generators/tables.d.ts +4 -0
- package/dist/sql-generator/generators/tables.js +65 -0
- package/dist/sql-generator/generators/triggers.d.ts +6 -0
- package/dist/sql-generator/generators/triggers.js +47 -0
- package/dist/sql-generator/index.d.ts +5 -0
- package/dist/sql-generator/index.js +3 -0
- package/dist/sql-generator/migration-planner.d.ts +15 -0
- package/dist/sql-generator/migration-planner.js +207 -0
- package/dist/sql-generator/migration-sql-generator.d.ts +9 -0
- package/dist/sql-generator/migration-sql-generator.js +181 -0
- package/dist/sql-generator/migration-types.d.ts +86 -0
- package/dist/sql-generator/migration-types.js +1 -0
- package/dist/sql-generator/sql-generator.d.ts +6 -0
- package/dist/sql-generator/sql-generator.js +26 -0
- package/dist/sql-generator/utils/ast-helpers.d.ts +58 -0
- package/dist/sql-generator/utils/ast-helpers.js +252 -0
- package/dist/sql-generator/utils/format.d.ts +2 -0
- package/dist/sql-generator/utils/format.js +21 -0
- package/dist/sql-generator/utils/snake-case.d.ts +3 -0
- package/dist/sql-generator/utils/snake-case.js +96 -0
- package/dist/sql-generator/utils/type-mapper.d.ts +2 -0
- package/dist/sql-generator/utils/type-mapper.js +39 -0
- package/dist/sql-generator/utils/value-formatter.d.ts +4 -0
- package/dist/sql-generator/utils/value-formatter.js +41 -0
- package/dist/types/generated-db.stub.d.ts +2 -0
- package/dist/types/generated-db.stub.js +3 -0
- package/dist/types/generated-policies.stub.d.ts +7 -0
- package/dist/types/generated-policies.stub.js +1 -0
- package/package.json +86 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export class UnauthorizedError extends Error {
|
|
2
|
+
constructor(message = 'Unauthorized') {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = 'UnauthorizedError';
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export class ForbiddenError extends Error {
|
|
8
|
+
constructor(message = 'Forbidden') {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'ForbiddenError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { UnauthorizedError } from './errors.js';
|
|
3
|
+
const BEARER_PREFIX = 'Bearer ';
|
|
4
|
+
const DEFAULT_ROLE_CLAIM = 'role';
|
|
5
|
+
const DEFAULT_USER_ID_CLAIM = 'sub';
|
|
6
|
+
const JWT_ALGORITHM = 'HS256';
|
|
7
|
+
export function createJwtResolver(options = {}) {
|
|
8
|
+
const roleClaim = options.roleClaim ?? process.env.JWT_ROLE_CLAIM ?? DEFAULT_ROLE_CLAIM;
|
|
9
|
+
const userIdClaim = options.userIdClaim ?? process.env.JWT_USER_ID_CLAIM ?? DEFAULT_USER_ID_CLAIM;
|
|
10
|
+
return async (c) => {
|
|
11
|
+
const authHeader = c.req.header('Authorization');
|
|
12
|
+
if (!authHeader?.startsWith(BEARER_PREFIX)) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const token = authHeader.slice(BEARER_PREFIX.length).trim();
|
|
16
|
+
if (!token) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const secret = options.secret ?? process.env.JWT_SECRET;
|
|
20
|
+
if (!secret) {
|
|
21
|
+
throw new UnauthorizedError('JWT_SECRET is not configured');
|
|
22
|
+
}
|
|
23
|
+
const payload = verifyJwt(token, secret);
|
|
24
|
+
const role = String(payload[roleClaim] ?? 'PUBLIC');
|
|
25
|
+
const userId = payload[userIdClaim];
|
|
26
|
+
if (userId === undefined || userId === null || userId === '') {
|
|
27
|
+
throw new UnauthorizedError(`JWT missing "${userIdClaim}" claim`);
|
|
28
|
+
}
|
|
29
|
+
const user = {
|
|
30
|
+
id: String(userId),
|
|
31
|
+
...payload,
|
|
32
|
+
};
|
|
33
|
+
return { role, user };
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function verifyJwt(token, secret) {
|
|
37
|
+
const parts = token.split('.');
|
|
38
|
+
if (parts.length !== 3) {
|
|
39
|
+
throw new UnauthorizedError('Invalid JWT format');
|
|
40
|
+
}
|
|
41
|
+
const [encodedHeader, encodedPayload, encodedSignature] = parts;
|
|
42
|
+
const signingInput = `${encodedHeader}.${encodedPayload}`;
|
|
43
|
+
const expectedSignature = createHmac('sha256', secret).update(signingInput).digest();
|
|
44
|
+
const actualSignature = base64UrlDecode(encodedSignature);
|
|
45
|
+
if (expectedSignature.length !== actualSignature.length ||
|
|
46
|
+
!timingSafeEqual(expectedSignature, actualSignature)) {
|
|
47
|
+
throw new UnauthorizedError('Invalid JWT signature');
|
|
48
|
+
}
|
|
49
|
+
const header = JSON.parse(base64UrlDecode(encodedHeader).toString('utf8'));
|
|
50
|
+
if (header.alg !== JWT_ALGORITHM) {
|
|
51
|
+
throw new UnauthorizedError(`Unsupported JWT algorithm "${header.alg ?? 'unknown'}"`);
|
|
52
|
+
}
|
|
53
|
+
return JSON.parse(base64UrlDecode(encodedPayload).toString('utf8'));
|
|
54
|
+
}
|
|
55
|
+
function base64UrlDecode(value) {
|
|
56
|
+
const normalized = value.replace(/-/g, '+').replace(/_/g, '/');
|
|
57
|
+
const padding = normalized.length % 4 === 0 ? '' : '='.repeat(4 - (normalized.length % 4));
|
|
58
|
+
return Buffer.from(normalized + padding, 'base64');
|
|
59
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createJwtResolver } from './jwt-resolver.js';
|
|
2
|
+
import { PUBLIC_ROLE } from './types.js';
|
|
3
|
+
const defaultPublicContext = { role: PUBLIC_ROLE };
|
|
4
|
+
export function createAuthMiddleware(resolver = createJwtResolver()) {
|
|
5
|
+
return async (c, next) => {
|
|
6
|
+
const auth = (await resolver(c)) ?? defaultPublicContext;
|
|
7
|
+
c.set('auth', auth);
|
|
8
|
+
await next();
|
|
9
|
+
};
|
|
10
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { WhereInput } from '../../db/where-translator.js';
|
|
2
|
+
import { type NormalizedPolicy, type PolicyOperation } from 'generated/policies.js';
|
|
3
|
+
import type { AuthContext } from './types.js';
|
|
4
|
+
export { ForbiddenError, UnauthorizedError } from './errors.js';
|
|
5
|
+
export declare function assertPolicy(model: string, role: string, operation: PolicyOperation): NormalizedPolicy;
|
|
6
|
+
export declare function resolvePolicyWhere(policy: NormalizedPolicy, auth: AuthContext): WhereInput | undefined;
|
|
7
|
+
export declare function mergeWhere(primary: Record<string, unknown>, policyWhere?: WhereInput): Record<string, unknown>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { POLICIES } from 'generated/policies.js';
|
|
2
|
+
import { ForbiddenError } from './errors.js';
|
|
3
|
+
import { interpolateTemplate } from './template.js';
|
|
4
|
+
import { PUBLIC_ROLE } from './types.js';
|
|
5
|
+
const SIMPLE_WHERE_PATTERN = /^(\w+)\s*(=|!=|<>|>=|<=|>|<)\s*(.+)$/;
|
|
6
|
+
export { ForbiddenError, UnauthorizedError } from './errors.js';
|
|
7
|
+
export function assertPolicy(model, role, operation) {
|
|
8
|
+
const policies = POLICIES[model];
|
|
9
|
+
if (!policies || policies.length === 0) {
|
|
10
|
+
throw new ForbiddenError(`No policies configured for model "${model}"`);
|
|
11
|
+
}
|
|
12
|
+
const policy = findPolicyForRole(policies, role);
|
|
13
|
+
if (!policy) {
|
|
14
|
+
throw new ForbiddenError(`Role "${role}" is not allowed to ${operation} ${model}`);
|
|
15
|
+
}
|
|
16
|
+
if (!isOperationAllowed(policy, operation)) {
|
|
17
|
+
throw new ForbiddenError(`Role "${role}" is not allowed to ${operation} ${model}`);
|
|
18
|
+
}
|
|
19
|
+
return policy;
|
|
20
|
+
}
|
|
21
|
+
export function resolvePolicyWhere(policy, auth) {
|
|
22
|
+
if (!policy.where) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
const interpolated = interpolateTemplate(policy.where, auth);
|
|
26
|
+
return parseSimpleWhereClause(interpolated);
|
|
27
|
+
}
|
|
28
|
+
export function mergeWhere(primary, policyWhere) {
|
|
29
|
+
if (!policyWhere || Object.keys(policyWhere).length === 0) {
|
|
30
|
+
return primary;
|
|
31
|
+
}
|
|
32
|
+
if (!primary || Object.keys(primary).length === 0) {
|
|
33
|
+
return policyWhere;
|
|
34
|
+
}
|
|
35
|
+
return { AND: [primary, policyWhere] };
|
|
36
|
+
}
|
|
37
|
+
function findPolicyForRole(policies, role) {
|
|
38
|
+
const directMatch = policies.find((policy) => policy.role === role);
|
|
39
|
+
if (directMatch) {
|
|
40
|
+
return directMatch;
|
|
41
|
+
}
|
|
42
|
+
if (role !== PUBLIC_ROLE) {
|
|
43
|
+
return policies.find((policy) => policy.role === PUBLIC_ROLE);
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
function isOperationAllowed(policy, operation) {
|
|
48
|
+
if (policy.operations === 'all') {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
return policy.operations.includes(operation);
|
|
52
|
+
}
|
|
53
|
+
function parseSimpleWhereClause(clause) {
|
|
54
|
+
const trimmedClause = clause.trim();
|
|
55
|
+
const match = trimmedClause.match(SIMPLE_WHERE_PATTERN);
|
|
56
|
+
if (!match) {
|
|
57
|
+
throw new ForbiddenError(`Unsupported policy where clause "${clause}". Only simple "field op value" forms are supported.`);
|
|
58
|
+
}
|
|
59
|
+
const [, field, operator, rawValue] = match;
|
|
60
|
+
const value = parseWhereValue(rawValue.trim());
|
|
61
|
+
if (operator === '=') {
|
|
62
|
+
return { [field]: value };
|
|
63
|
+
}
|
|
64
|
+
if (operator === '!=' || operator === '<>') {
|
|
65
|
+
return { NOT: { [field]: value } };
|
|
66
|
+
}
|
|
67
|
+
const operatorMap = {
|
|
68
|
+
'>': 'gt',
|
|
69
|
+
'>=': 'gte',
|
|
70
|
+
'<': 'lt',
|
|
71
|
+
'<=': 'lte',
|
|
72
|
+
};
|
|
73
|
+
const mappedOperator = operatorMap[operator];
|
|
74
|
+
if (!mappedOperator) {
|
|
75
|
+
throw new ForbiddenError(`Unsupported policy where operator "${operator}"`);
|
|
76
|
+
}
|
|
77
|
+
return { [field]: { [mappedOperator]: value } };
|
|
78
|
+
}
|
|
79
|
+
function parseWhereValue(rawValue) {
|
|
80
|
+
if ((rawValue.startsWith("'") && rawValue.endsWith("'")) ||
|
|
81
|
+
(rawValue.startsWith('"') && rawValue.endsWith('"'))) {
|
|
82
|
+
return rawValue.slice(1, -1);
|
|
83
|
+
}
|
|
84
|
+
if (rawValue === 'true') {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
if (rawValue === 'false') {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
const numericValue = Number(rawValue);
|
|
91
|
+
if (!Number.isNaN(numericValue) && rawValue !== '') {
|
|
92
|
+
return numericValue;
|
|
93
|
+
}
|
|
94
|
+
return rawValue;
|
|
95
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ForbiddenError } from './errors.js';
|
|
2
|
+
const TEMPLATE_PATTERN = /\{\{([^}]+)\}\}/g;
|
|
3
|
+
export function interpolateTemplate(template, auth) {
|
|
4
|
+
const root = { auth };
|
|
5
|
+
return template.replace(TEMPLATE_PATTERN, (_, rawPath) => {
|
|
6
|
+
const path = rawPath.trim();
|
|
7
|
+
const value = resolveAuthPath(root, path);
|
|
8
|
+
if (value === undefined || value === null) {
|
|
9
|
+
throw new ForbiddenError(`Missing auth context value for "${path}"`);
|
|
10
|
+
}
|
|
11
|
+
return String(value);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
function resolveAuthPath(root, path) {
|
|
15
|
+
const segments = path.split('.').filter(Boolean);
|
|
16
|
+
let current = root;
|
|
17
|
+
for (const segment of segments) {
|
|
18
|
+
if (current === null || current === undefined || typeof current !== 'object') {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
current = current[segment];
|
|
22
|
+
}
|
|
23
|
+
return current;
|
|
24
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Context } from 'hono';
|
|
2
|
+
import type { AppEnv } from '../types.js';
|
|
3
|
+
export declare const PUBLIC_ROLE = "PUBLIC";
|
|
4
|
+
export interface AuthUser {
|
|
5
|
+
id: string;
|
|
6
|
+
[key: string]: unknown;
|
|
7
|
+
}
|
|
8
|
+
export interface AuthContext {
|
|
9
|
+
role: string;
|
|
10
|
+
user?: AuthUser;
|
|
11
|
+
}
|
|
12
|
+
export type AuthResolver = (c: Context<AppEnv>) => Promise<AuthContext | null>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const PUBLIC_ROLE = 'PUBLIC';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Pool } from 'pg';
|
|
2
|
+
import type { MiddlewareHandler } from 'hono';
|
|
3
|
+
import type { DbClient } from 'generated/db.js';
|
|
4
|
+
import type { AppEnv } from '../types.js';
|
|
5
|
+
export declare const db: DbClient;
|
|
6
|
+
export declare function createDbMiddleware(options?: {
|
|
7
|
+
pool?: Pool;
|
|
8
|
+
}): MiddlewareHandler<AppEnv>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Pool } from 'pg';
|
|
2
|
+
import { createDbClient } from 'generated/db.js';
|
|
3
|
+
import { getDatabaseUrl } from '../../db/config.js';
|
|
4
|
+
const pool = new Pool({ connectionString: getDatabaseUrl() });
|
|
5
|
+
export const db = createDbClient(pool);
|
|
6
|
+
export function createDbMiddleware(options = {}) {
|
|
7
|
+
const client = options.pool ? createDbClient(options.pool) : db;
|
|
8
|
+
return async (c, next) => {
|
|
9
|
+
c.set('db', client);
|
|
10
|
+
await next();
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ForbiddenError, UnauthorizedError } from '../auth/errors.js';
|
|
2
|
+
import { DatabaseError, ForeignKeyConstraintError, UniqueConstraintError, } from '../../db/errors.js';
|
|
3
|
+
export const handleError = (error, c) => {
|
|
4
|
+
if (error instanceof UnauthorizedError) {
|
|
5
|
+
return c.json({ error: error.message }, 401);
|
|
6
|
+
}
|
|
7
|
+
if (error instanceof ForbiddenError) {
|
|
8
|
+
return c.json({ error: error.message }, 403);
|
|
9
|
+
}
|
|
10
|
+
if (error instanceof UniqueConstraintError) {
|
|
11
|
+
return c.json({ error: error.message }, 409);
|
|
12
|
+
}
|
|
13
|
+
if (error instanceof ForeignKeyConstraintError) {
|
|
14
|
+
return c.json({ error: error.message }, 400);
|
|
15
|
+
}
|
|
16
|
+
if (error instanceof DatabaseError) {
|
|
17
|
+
return c.json({ error: error.message }, 500);
|
|
18
|
+
}
|
|
19
|
+
if (error instanceof Error && error.message.includes('returned no rows')) {
|
|
20
|
+
return c.json({ error: 'Not found' }, 404);
|
|
21
|
+
}
|
|
22
|
+
console.error(error);
|
|
23
|
+
return c.json({ error: 'Internal server error' }, 500);
|
|
24
|
+
};
|
|
25
|
+
export function notFoundResponse(c) {
|
|
26
|
+
return c.json({ error: 'Not found' }, 404);
|
|
27
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ValidationTargets } from 'hono';
|
|
2
|
+
import type { ZodSchema } from 'zod';
|
|
3
|
+
export declare function validateJson<T extends ZodSchema>(schema: T): import("hono").MiddlewareHandler<import("hono").Env, string, {
|
|
4
|
+
in: (undefined extends (T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").input<T> : never) ? true : false) extends true ? {
|
|
5
|
+
json?: ([T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").input<T> : never] extends [any] ? T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").input<T> : never : [Exclude<T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").input<T> : never, undefined>] extends [never] ? {} : [Exclude<T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").input<T> : never, undefined>] extends [object] ? undefined extends (T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").input<T> : never) ? ((T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").input<T> : never) & undefined) | (((Exclude<T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").input<T> : never, (T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").input<T> : never) & undefined> extends infer T_3 ? { [K_2 in keyof T_3]: ([Exclude<T_3[K_2], undefined>] extends [string] ? [string & Exclude<T_3[K_2], undefined>] extends [import("hono/utils/types").UnionToIntersection<string & Exclude<T_3[K_2], undefined>>] ? false : true : false) extends true ? T_3[K_2] : ([unknown] extends [T_3[K_2]] ? false : undefined extends T_3[K_2] ? true : false) extends true ? T_3[K_2] : Target extends "form" ? T$1 | T$1[] : Target extends "query" ? string | string[] : Target extends "param" ? string : Target extends "header" ? string : Target extends "cookie" ? string : unknown; } : never) extends infer T_2 ? { [K_1 in keyof T_2]: T_2[K_1]; } : never) extends infer T_1 ? { [K in keyof T_1]: T_1[K]; } : never) : (((T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").input<T> : never) extends infer T_6 ? { [K_5 in keyof T_6]: ([Exclude<T_6[K_5], undefined>] extends [string] ? [string & Exclude<T_6[K_5], undefined>] extends [import("hono/utils/types").UnionToIntersection<string & Exclude<T_6[K_5], undefined>>] ? false : true : false) extends true ? T_6[K_5] : ([unknown] extends [T_6[K_5]] ? false : undefined extends T_6[K_5] ? true : false) extends true ? T_6[K_5] : Target extends "form" ? T$1 | T$1[] : Target extends "query" ? string | string[] : Target extends "param" ? string : Target extends "header" ? string : Target extends "cookie" ? string : unknown; } : never) extends infer T_5 ? { [K_4 in keyof T_5]: T_5[K_4]; } : never) extends infer T_4 ? { [K_3 in keyof T_4]: T_4[K_3]; } : never : {}) | undefined;
|
|
6
|
+
} : {
|
|
7
|
+
json: [T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").input<T> : never] extends [any] ? T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").input<T> : never : [Exclude<T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").input<T> : never, undefined>] extends [never] ? {} : [Exclude<T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").input<T> : never, undefined>] extends [object] ? undefined extends (T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").input<T> : never) ? ((T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").input<T> : never) & undefined) | (((Exclude<T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").input<T> : never, (T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").input<T> : never) & undefined> extends infer T_9 ? { [K_2 in keyof T_9]: ([Exclude<T_9[K_2], undefined>] extends [string] ? [string & Exclude<T_9[K_2], undefined>] extends [import("hono/utils/types").UnionToIntersection<string & Exclude<T_9[K_2], undefined>>] ? false : true : false) extends true ? T_9[K_2] : ([unknown] extends [T_9[K_2]] ? false : undefined extends T_9[K_2] ? true : false) extends true ? T_9[K_2] : Target extends "form" ? T$1 | T$1[] : Target extends "query" ? string | string[] : Target extends "param" ? string : Target extends "header" ? string : Target extends "cookie" ? string : unknown; } : never) extends infer T_8 ? { [K_1 in keyof T_8]: T_8[K_1]; } : never) extends infer T_7 ? { [K in keyof T_7]: T_7[K]; } : never) : (((T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").input<T> : never) extends infer T_12 ? { [K_5 in keyof T_12]: ([Exclude<T_12[K_5], undefined>] extends [string] ? [string & Exclude<T_12[K_5], undefined>] extends [import("hono/utils/types").UnionToIntersection<string & Exclude<T_12[K_5], undefined>>] ? false : true : false) extends true ? T_12[K_5] : ([unknown] extends [T_12[K_5]] ? false : undefined extends T_12[K_5] ? true : false) extends true ? T_12[K_5] : Target extends "form" ? T$1 | T$1[] : Target extends "query" ? string | string[] : Target extends "param" ? string : Target extends "header" ? string : Target extends "cookie" ? string : unknown; } : never) extends infer T_11 ? { [K_4 in keyof T_11]: T_11[K_4]; } : never) extends infer T_10 ? { [K_3 in keyof T_10]: T_10[K_3]; } : never : {};
|
|
8
|
+
};
|
|
9
|
+
out: {
|
|
10
|
+
json: T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").output<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").infer<T> : never;
|
|
11
|
+
};
|
|
12
|
+
}, Response>;
|
|
13
|
+
export declare function validateParam<T extends ZodSchema>(schema: T): import("hono").MiddlewareHandler<import("hono").Env, string, {
|
|
14
|
+
in: (undefined extends (T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").input<T> : never) ? true : false) extends true ? {
|
|
15
|
+
param?: ([T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").input<T> : never] extends [Record<string, string>] ? Record<string, string> & (T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").input<T> : never) : [Exclude<T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").input<T> : never, undefined>] extends [never] ? {} : [Exclude<T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").input<T> : never, undefined>] extends [object] ? undefined extends (T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").input<T> : never) ? ((T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").input<T> : never) & undefined) | (((Exclude<T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").input<T> : never, (T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").input<T> : never) & undefined> extends infer T_3 ? { [K_2 in keyof T_3]: ([Exclude<T_3[K_2], undefined>] extends [string] ? [string & Exclude<T_3[K_2], undefined>] extends [import("hono/utils/types").UnionToIntersection<string & Exclude<T_3[K_2], undefined>>] ? false : true : false) extends true ? T_3[K_2] : ([unknown] extends [T_3[K_2]] ? false : undefined extends T_3[K_2] ? true : false) extends true ? T_3[K_2] : Target extends "form" ? T$1 | T$1[] : Target extends "query" ? string | string[] : Target extends "param" ? string : Target extends "header" ? string : Target extends "cookie" ? string : unknown; } : never) extends infer T_2 ? { [K_1 in keyof T_2]: T_2[K_1]; } : never) extends infer T_1 ? { [K in keyof T_1]: T_1[K]; } : never) : (((T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").input<T> : never) extends infer T_6 ? { [K_5 in keyof T_6]: ([Exclude<T_6[K_5], undefined>] extends [string] ? [string & Exclude<T_6[K_5], undefined>] extends [import("hono/utils/types").UnionToIntersection<string & Exclude<T_6[K_5], undefined>>] ? false : true : false) extends true ? T_6[K_5] : ([unknown] extends [T_6[K_5]] ? false : undefined extends T_6[K_5] ? true : false) extends true ? T_6[K_5] : Target extends "form" ? T$1 | T$1[] : Target extends "query" ? string | string[] : Target extends "param" ? string : Target extends "header" ? string : Target extends "cookie" ? string : unknown; } : never) extends infer T_5 ? { [K_4 in keyof T_5]: T_5[K_4]; } : never) extends infer T_4 ? { [K_3 in keyof T_4]: T_4[K_3]; } : never : {}) | undefined;
|
|
16
|
+
} : {
|
|
17
|
+
param: [T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").input<T> : never] extends [Record<string, string>] ? Record<string, string> & (T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").input<T> : never) : [Exclude<T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").input<T> : never, undefined>] extends [never] ? {} : [Exclude<T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").input<T> : never, undefined>] extends [object] ? undefined extends (T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").input<T> : never) ? ((T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").input<T> : never) & undefined) | (((Exclude<T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").input<T> : never, (T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").input<T> : never) & undefined> extends infer T_9 ? { [K_2 in keyof T_9]: ([Exclude<T_9[K_2], undefined>] extends [string] ? [string & Exclude<T_9[K_2], undefined>] extends [import("hono/utils/types").UnionToIntersection<string & Exclude<T_9[K_2], undefined>>] ? false : true : false) extends true ? T_9[K_2] : ([unknown] extends [T_9[K_2]] ? false : undefined extends T_9[K_2] ? true : false) extends true ? T_9[K_2] : Target extends "form" ? T$1 | T$1[] : Target extends "query" ? string | string[] : Target extends "param" ? string : Target extends "header" ? string : Target extends "cookie" ? string : unknown; } : never) extends infer T_8 ? { [K_1 in keyof T_8]: T_8[K_1]; } : never) extends infer T_7 ? { [K in keyof T_7]: T_7[K]; } : never) : (((T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").input<T> : never) extends infer T_12 ? { [K_5 in keyof T_12]: ([Exclude<T_12[K_5], undefined>] extends [string] ? [string & Exclude<T_12[K_5], undefined>] extends [import("hono/utils/types").UnionToIntersection<string & Exclude<T_12[K_5], undefined>>] ? false : true : false) extends true ? T_12[K_5] : ([unknown] extends [T_12[K_5]] ? false : undefined extends T_12[K_5] ? true : false) extends true ? T_12[K_5] : Target extends "form" ? T$1 | T$1[] : Target extends "query" ? string | string[] : Target extends "param" ? string : Target extends "header" ? string : Target extends "cookie" ? string : unknown; } : never) extends infer T_11 ? { [K_4 in keyof T_11]: T_11[K_4]; } : never) extends infer T_10 ? { [K_3 in keyof T_10]: T_10[K_3]; } : never : {};
|
|
18
|
+
};
|
|
19
|
+
out: {
|
|
20
|
+
param: T extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").output<T> : T extends import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>> ? import("zod").infer<T> : never;
|
|
21
|
+
};
|
|
22
|
+
}, Response>;
|
|
23
|
+
export type ValidationTarget = keyof ValidationTargets;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { zValidator } from '@hono/zod-validator';
|
|
2
|
+
function validationHook(result, c) {
|
|
3
|
+
if (!result.success) {
|
|
4
|
+
const message = result.error.issues[0]?.message ?? 'Validation failed';
|
|
5
|
+
return c.json({ error: message }, 400);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export function validateJson(schema) {
|
|
9
|
+
return zValidator('json', schema, validationHook);
|
|
10
|
+
}
|
|
11
|
+
export function validateParam(schema) {
|
|
12
|
+
return zValidator('param', schema, validationHook);
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { pluralize } from '../../db/utils/naming.js';
|
|
2
|
+
function toKebabCase(value) {
|
|
3
|
+
return value.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
|
|
4
|
+
}
|
|
5
|
+
export function toRouteBasePath(modelName) {
|
|
6
|
+
const camel = modelName.charAt(0).toLowerCase() + modelName.slice(1);
|
|
7
|
+
return pluralize(toKebabCase(camel));
|
|
8
|
+
}
|
|
9
|
+
export function toRouteFileName(modelName) {
|
|
10
|
+
return `${toRouteBasePath(modelName)}.ts`;
|
|
11
|
+
}
|
|
12
|
+
function segmentToCamelCase(segment, capitalizeFirst) {
|
|
13
|
+
const kebabCamel = segment.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
|
|
14
|
+
if (!capitalizeFirst) {
|
|
15
|
+
return kebabCamel;
|
|
16
|
+
}
|
|
17
|
+
return kebabCamel.charAt(0).toUpperCase() + kebabCamel.slice(1);
|
|
18
|
+
}
|
|
19
|
+
export function toRouteImportName(basePath) {
|
|
20
|
+
const segments = basePath.split('/');
|
|
21
|
+
const camel = segments
|
|
22
|
+
.map((segment, index) => segmentToCamelCase(segment, index > 0))
|
|
23
|
+
.join('');
|
|
24
|
+
return `${camel}Router`;
|
|
25
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Schema } from '../schema-dsl/ast.js';
|
|
2
|
+
export interface AppGeneratorOptions {
|
|
3
|
+
customRoutesDir?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare class AppGenerator {
|
|
6
|
+
private readonly schema;
|
|
7
|
+
private readonly options;
|
|
8
|
+
constructor(schema: Schema, options?: AppGeneratorOptions);
|
|
9
|
+
generate(): string;
|
|
10
|
+
}
|
|
11
|
+
export declare function generateAppFile(schema: Schema, options?: AppGeneratorOptions): string;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { PACKAGE_NAME } from '../constants.js';
|
|
3
|
+
import { discoverCustomRoutes } from './custom-route-scanner.js';
|
|
4
|
+
import { getRouteMountEntries } from './route-generator.js';
|
|
5
|
+
const DEFAULT_CUSTOM_ROUTES_DIR = path.resolve('src/routes');
|
|
6
|
+
export class AppGenerator {
|
|
7
|
+
schema;
|
|
8
|
+
options;
|
|
9
|
+
constructor(schema, options = {}) {
|
|
10
|
+
this.schema = schema;
|
|
11
|
+
this.options = options;
|
|
12
|
+
}
|
|
13
|
+
generate() {
|
|
14
|
+
const mounts = getRouteMountEntries(this.schema);
|
|
15
|
+
const generatedImports = mounts
|
|
16
|
+
.map((entry) => `import ${entry.importName} from './routes/${entry.fileName.replace(/\.ts$/, '.js')}';`)
|
|
17
|
+
.join('\n');
|
|
18
|
+
const generatedRoutes = mounts
|
|
19
|
+
.map((entry) => ` app.route('/${entry.basePath}', ${entry.importName});`)
|
|
20
|
+
.join('\n');
|
|
21
|
+
const customRoutesDir = this.options.customRoutesDir ?? DEFAULT_CUSTOM_ROUTES_DIR;
|
|
22
|
+
const customMounts = discoverCustomRoutes(customRoutesDir);
|
|
23
|
+
const customImports = customMounts
|
|
24
|
+
.map((entry) => `import ${entry.importName} from '${entry.importPath}';`)
|
|
25
|
+
.join('\n');
|
|
26
|
+
const customRoutes = customMounts
|
|
27
|
+
.map((entry) => ` app.route('/${entry.basePath}', ${entry.importName});`)
|
|
28
|
+
.join('\n');
|
|
29
|
+
const routeImports = [generatedImports, customImports].filter(Boolean).join('\n');
|
|
30
|
+
const routeMounts = [generatedRoutes, customRoutes].filter(Boolean).join('\n');
|
|
31
|
+
return [
|
|
32
|
+
'// Auto-generated by AppGenerator. Do not edit manually.',
|
|
33
|
+
"import { pathToFileURL } from 'node:url';",
|
|
34
|
+
"import { Pool } from 'pg';",
|
|
35
|
+
"import { Hono } from 'hono';",
|
|
36
|
+
"import { logger } from 'hono/logger';",
|
|
37
|
+
"import { prettyJSON } from 'hono/pretty-json';",
|
|
38
|
+
"import { serve } from '@hono/node-server';",
|
|
39
|
+
routeImports,
|
|
40
|
+
`import { createAuthMiddleware } from '${PACKAGE_NAME}/api/auth/middleware';`,
|
|
41
|
+
`import { createJwtResolver } from '${PACKAGE_NAME}/api/auth/jwt-resolver';`,
|
|
42
|
+
`import type { AuthResolver } from '${PACKAGE_NAME}/api/auth/types';`,
|
|
43
|
+
`import { createDbMiddleware } from '${PACKAGE_NAME}/api/middleware/db';`,
|
|
44
|
+
`import { handleError } from '${PACKAGE_NAME}/api/middleware/errors';`,
|
|
45
|
+
`import type { AppEnv } from '${PACKAGE_NAME}/api/types';`,
|
|
46
|
+
'',
|
|
47
|
+
'export interface CreateAppOptions {',
|
|
48
|
+
' pool?: Pool;',
|
|
49
|
+
' authResolver?: AuthResolver;',
|
|
50
|
+
'}',
|
|
51
|
+
'',
|
|
52
|
+
'export function createApp(options: CreateAppOptions = {}): Hono<AppEnv> {',
|
|
53
|
+
' const app = new Hono<AppEnv>();',
|
|
54
|
+
' app.use(logger());',
|
|
55
|
+
' app.use(prettyJSON());',
|
|
56
|
+
' app.use(createDbMiddleware({ pool: options.pool }));',
|
|
57
|
+
' app.use(createAuthMiddleware(options.authResolver ?? createJwtResolver()));',
|
|
58
|
+
' app.onError(handleError);',
|
|
59
|
+
'',
|
|
60
|
+
routeMounts,
|
|
61
|
+
'',
|
|
62
|
+
' return app;',
|
|
63
|
+
'}',
|
|
64
|
+
'',
|
|
65
|
+
'const isMain = import.meta.url === pathToFileURL(process.argv[1] ?? \'\').href;',
|
|
66
|
+
'',
|
|
67
|
+
'if (isMain) {',
|
|
68
|
+
' const port = Number(process.env.PORT ?? 3000);',
|
|
69
|
+
' serve({ fetch: createApp().fetch, port }, () => {',
|
|
70
|
+
' console.log(`Server running at http://localhost:${port}`);',
|
|
71
|
+
' });',
|
|
72
|
+
'}',
|
|
73
|
+
'',
|
|
74
|
+
].join('\n');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function generateAppFile(schema, options) {
|
|
78
|
+
return new AppGenerator(schema, options).generate();
|
|
79
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { toRouteImportName } from '../api/utils/route-naming.js';
|
|
4
|
+
function isRouteFile(filename) {
|
|
5
|
+
return (filename.endsWith('.ts') &&
|
|
6
|
+
!filename.endsWith('.test.ts') &&
|
|
7
|
+
!filename.endsWith('.d.ts') &&
|
|
8
|
+
!filename.startsWith('_'));
|
|
9
|
+
}
|
|
10
|
+
function isSkippedDir(dirname) {
|
|
11
|
+
return dirname.startsWith('_');
|
|
12
|
+
}
|
|
13
|
+
function scanDirectory(customRoutesDir, relativeDir, entries) {
|
|
14
|
+
const absoluteDir = relativeDir ? path.join(customRoutesDir, relativeDir) : customRoutesDir;
|
|
15
|
+
for (const entry of readdirSync(absoluteDir)) {
|
|
16
|
+
const entryRelativePath = relativeDir ? `${relativeDir}/${entry}` : entry;
|
|
17
|
+
const absolutePath = path.join(customRoutesDir, entryRelativePath);
|
|
18
|
+
if (statSync(absolutePath).isDirectory()) {
|
|
19
|
+
if (!isSkippedDir(entry)) {
|
|
20
|
+
scanDirectory(customRoutesDir, entryRelativePath, entries);
|
|
21
|
+
}
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (!isRouteFile(entry)) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const basePath = entryRelativePath.replace(/\.ts$/, '').replace(/\\/g, '/');
|
|
28
|
+
entries.push({
|
|
29
|
+
basePath,
|
|
30
|
+
importName: toRouteImportName(basePath),
|
|
31
|
+
importPath: `../src/routes/${basePath}.js`,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function discoverCustomRoutes(customRoutesDir) {
|
|
36
|
+
if (!existsSync(customRoutesDir)) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
const entries = [];
|
|
40
|
+
scanDirectory(customRoutesDir, '', entries);
|
|
41
|
+
return entries.sort((left, right) => left.basePath.localeCompare(right.basePath));
|
|
42
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parse } from '../schema-dsl/index.js';
|
|
4
|
+
import { generateApiFiles } from './index.js';
|
|
5
|
+
const DEFAULT_SCHEMA_PATH = path.resolve('app.schema');
|
|
6
|
+
const DEFAULT_CUSTOM_ROUTES_DIR = path.resolve('src/routes');
|
|
7
|
+
const OUTPUT_DIR = path.resolve('generated');
|
|
8
|
+
const ROUTES_DIR = path.join(OUTPUT_DIR, 'routes');
|
|
9
|
+
const SCHEMAS_DIR = path.join(OUTPUT_DIR, 'schemas');
|
|
10
|
+
async function main() {
|
|
11
|
+
const schemaPath = process.argv[2] ?? DEFAULT_SCHEMA_PATH;
|
|
12
|
+
const source = await readFile(schemaPath, 'utf8');
|
|
13
|
+
const schema = parse(source);
|
|
14
|
+
const files = generateApiFiles(schema, { customRoutesDir: DEFAULT_CUSTOM_ROUTES_DIR });
|
|
15
|
+
await mkdir(ROUTES_DIR, { recursive: true });
|
|
16
|
+
await mkdir(SCHEMAS_DIR, { recursive: true });
|
|
17
|
+
await writeFile(path.join(OUTPUT_DIR, 'app.ts'), files.app, 'utf8');
|
|
18
|
+
await writeFile(path.join(OUTPUT_DIR, 'policies.ts'), files.policies, 'utf8');
|
|
19
|
+
await writeFile(path.join(SCHEMAS_DIR, 'validation.ts'), files.validation, 'utf8');
|
|
20
|
+
for (const [fileName, content] of files.routes) {
|
|
21
|
+
await writeFile(path.join(ROUTES_DIR, fileName), content, 'utf8');
|
|
22
|
+
}
|
|
23
|
+
console.log(`Generated API files in ${OUTPUT_DIR}`);
|
|
24
|
+
}
|
|
25
|
+
main().catch((error) => {
|
|
26
|
+
console.error(error instanceof Error ? error.message : error);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Schema } from '../schema-dsl/ast.js';
|
|
2
|
+
export interface GeneratedApiFiles {
|
|
3
|
+
app: string;
|
|
4
|
+
policies: string;
|
|
5
|
+
validation: string;
|
|
6
|
+
routes: Map<string, string>;
|
|
7
|
+
}
|
|
8
|
+
export interface GenerateApiFilesOptions {
|
|
9
|
+
customRoutesDir?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function generateApiFiles(schema: Schema, options?: GenerateApiFilesOptions): GeneratedApiFiles;
|