spfn 0.2.0-beta.6 → 0.2.0-beta.60
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 +1 -1
- package/README.md +371 -211
- package/bin/spfn.js +53 -4
- package/dist/index.js +2791 -2085
- package/dist/templates/.dockerignore +1 -0
- package/dist/templates/Dockerfile +1 -1
- package/dist/templates/README.md +81 -0
- package/dist/templates/lib/api-client.ts +66 -33
- package/dist/templates/server/config/env.config.ts +10 -75
- package/dist/templates/server/entities/config.ts +1 -1
- package/dist/templates/server/entities/example.entity.ts +1 -1
- package/dist/templates/server/repositories/example.repository.ts +1 -1
- package/dist/templates/server/router.ts +2 -3
- package/dist/templates/server/routes/examples.ts +2 -12
- package/dist/templates/server/routes/health.ts +1 -1
- package/dist/templates/server/routes/root.ts +1 -2
- package/dist/templates/server/server.config.ts +2 -2
- package/package.json +29 -18
- package/dist/commands/generate/templates/contract.template +0 -58
- package/dist/commands/generate/templates/entity.template +0 -28
- package/dist/commands/generate/templates/init-migration.template +0 -3
- package/dist/commands/generate/templates/repository.template +0 -56
- package/dist/commands/generate/templates/route.template +0 -65
- package/dist/commands/generate/templates/schema.template +0 -13
- package/dist/templates/Dockerfile.optimized +0 -67
- package/dist/templates/config/index.ts +0 -22
- package/dist/templates/config/schema.ts +0 -42
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# {{projectName}}
|
|
2
|
+
|
|
3
|
+
A full-stack app built with [SPFN](https://github.com/spfn/spfn) — a typed SPFN
|
|
4
|
+
backend running alongside Next.js.
|
|
5
|
+
|
|
6
|
+
## Getting started
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
# 1. Start PostgreSQL + Redis
|
|
10
|
+
docker compose up -d
|
|
11
|
+
|
|
12
|
+
# 2. The env files are generated for you:
|
|
13
|
+
# .env.local — Next.js-facing values (gitignored)
|
|
14
|
+
# .env.server — server secrets: DB, cache (gitignored, never loaded by Next.js)
|
|
15
|
+
# Review them and adjust as needed.
|
|
16
|
+
|
|
17
|
+
# 3. Start the dev server (Next.js :3790 + SPFN API :8790)
|
|
18
|
+
{{pmRun}} spfn:dev
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Then open:
|
|
22
|
+
|
|
23
|
+
- Next.js — http://localhost:3790
|
|
24
|
+
- API health — http://localhost:8790/health
|
|
25
|
+
|
|
26
|
+
## Project structure
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
src/
|
|
30
|
+
app/ Next.js App Router
|
|
31
|
+
server/ SPFN backend (entities, repositories, routes, router)
|
|
32
|
+
generated/ codegen output (route map) — do not edit by hand
|
|
33
|
+
.env.local Next.js-facing env (gitignored)
|
|
34
|
+
.env.server server secrets (gitignored)
|
|
35
|
+
.env.example committed reference — keys only, placeholder values
|
|
36
|
+
.spfnrc.ts codegen configuration
|
|
37
|
+
spfn.config.js deployment config
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
A feature is a vertical slice: `Entity` (Drizzle table) → `Repository` →
|
|
41
|
+
`Route` (TypeBox validation) → `Router` → `{{pmRun}} codegen` → typed client.
|
|
42
|
+
|
|
43
|
+
## Common commands
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
{{pmRun}} spfn:dev # dev: Next.js + SPFN API (add --watch to restart on changes)
|
|
47
|
+
{{pmRun}} spfn:build # production build
|
|
48
|
+
{{pmRun}} spfn:start # run the production build
|
|
49
|
+
{{pm}} spfn db generate # create a migration from schema changes
|
|
50
|
+
{{pm}} spfn db migrate # apply pending migrations
|
|
51
|
+
{{pm}} spfn env check # check .env files against the schema
|
|
52
|
+
{{pm}} spfn secret set DB_URL # store a secret (keychain locally, SOPS for deploys)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Environment & secrets
|
|
56
|
+
|
|
57
|
+
`.env.server` holds server-only secrets and is gitignored — Next.js never loads it.
|
|
58
|
+
For a managed workflow use `spfn secret`: local values go to the OS keychain (only a
|
|
59
|
+
`secret:keychain:` reference lands in `.env.server`), and deployed secrets are stored
|
|
60
|
+
in encrypted SOPS files. See the [SPFN CLI docs](https://github.com/spfn/spfn).
|
|
61
|
+
|
|
62
|
+
<!-- {{#auth}} -->
|
|
63
|
+
## Authentication
|
|
64
|
+
|
|
65
|
+
This project includes `@spfn/auth`. Configure providers and session settings in your
|
|
66
|
+
server setup, and read `@spfn/auth`'s README for the full Entity → Repository →
|
|
67
|
+
Service → Route flow and the typed `authApi` client.
|
|
68
|
+
<!-- {{/auth}} -->
|
|
69
|
+
|
|
70
|
+
## Deployment
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Build + run locally
|
|
74
|
+
{{pmRun}} spfn:build && {{pmRun}} spfn:start
|
|
75
|
+
|
|
76
|
+
# Or with Docker
|
|
77
|
+
docker compose -f docker-compose.production.yml up --build -d
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Never commit real secrets. `.env.server` is gitignored; inject secrets in your
|
|
81
|
+
CI/CD pipeline or via `spfn secret` + your GitOps decryption step.
|
|
@@ -1,47 +1,80 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* API Client Configuration
|
|
3
3
|
*
|
|
4
|
-
* Type-safe client for accessing server routes
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { createApi } from '@spfn/core/nextjs';
|
|
8
|
-
import { errorRegistry } from "@spfn/core/errors";
|
|
9
|
-
|
|
10
|
-
import type { AppRouter } from '@/server/router';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Pre-configured type-safe API client
|
|
14
|
-
*
|
|
4
|
+
* Type-safe client for accessing server routes.
|
|
15
5
|
* Core HTTP errors are automatically registered.
|
|
16
|
-
* Add custom application errors via the errors field if needed.
|
|
17
6
|
*
|
|
18
7
|
* @example
|
|
19
8
|
* ```typescript
|
|
20
9
|
* import { api } from '@/lib/api-client';
|
|
21
10
|
*
|
|
22
11
|
* // Basic call
|
|
23
|
-
* const user = await api.getUser
|
|
24
|
-
* .call({ id: '123' });
|
|
12
|
+
* const user = await api.getUser.call({ params: { id: '123' } });
|
|
25
13
|
*
|
|
26
|
-
* //
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* }
|
|
30
|
-
*
|
|
31
|
-
* console.log('Resource not found');
|
|
32
|
-
* } else if (error instanceof ValidationError) {
|
|
33
|
-
* console.log('Validation failed:', error.fields);
|
|
34
|
-
* }
|
|
35
|
-
* }
|
|
14
|
+
* // With options
|
|
15
|
+
* const data = await api.getData
|
|
16
|
+
* .headers({ 'X-Custom': 'value' })
|
|
17
|
+
* .fetchOptions({ next: { revalidate: 60 } })
|
|
18
|
+
* .call({ query: { page: 1 } });
|
|
36
19
|
* ```
|
|
37
|
-
*
|
|
38
|
-
* Works in:
|
|
39
|
-
* - Server Components (no router import needed)
|
|
40
|
-
* - Client Components (proxied through /api/actions)
|
|
41
|
-
* - Server Actions
|
|
42
|
-
* - Route Handlers
|
|
43
20
|
*/
|
|
21
|
+
|
|
22
|
+
import { createApi } from '@spfn/core/nextjs';
|
|
23
|
+
import type { AppRouter } from '@/server/router';
|
|
24
|
+
|
|
44
25
|
export const api = createApi<AppRouter>({
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Base URL for RPC endpoint
|
|
28
|
+
* @default '/api/rpc'
|
|
29
|
+
*/
|
|
30
|
+
// baseUrl: '/api/rpc',
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Default headers for all requests
|
|
34
|
+
*/
|
|
35
|
+
// headers: { 'X-Client': 'web' },
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Request timeout in milliseconds
|
|
39
|
+
* @default 30000
|
|
40
|
+
*/
|
|
41
|
+
// timeout: 30000,
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Custom error registry for deserialization
|
|
45
|
+
* Core HTTP errors are automatically included.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* import { authErrorRegistry } from '@myapp/auth/errors';
|
|
49
|
+
* errorRegistry: [authErrorRegistry, MyCustomError]
|
|
50
|
+
*/
|
|
51
|
+
// errorRegistry: [],
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Global request interceptor
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* onRequest: async (url, init) => {
|
|
58
|
+
* init.headers = { ...init.headers, 'X-Request-Id': crypto.randomUUID() };
|
|
59
|
+
* return init;
|
|
60
|
+
* }
|
|
61
|
+
*/
|
|
62
|
+
// onRequest: async (url, init) => init,
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Global response interceptor
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* onResponse: async (response, body) => {
|
|
69
|
+
* if (response.status === 401) redirect('/login');
|
|
70
|
+
* return { response, body };
|
|
71
|
+
* }
|
|
72
|
+
*/
|
|
73
|
+
// onResponse: async (response, body) => ({ response, body }),
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Enable debug logging
|
|
77
|
+
* @default false
|
|
78
|
+
*/
|
|
79
|
+
debug: process.env.NODE_ENV === 'development',
|
|
80
|
+
});
|
|
@@ -36,88 +36,23 @@ export const envSchema = defineEnvSchema({
|
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
* Type-safe Environment
|
|
39
|
+
* Type-safe Environment Variables
|
|
40
40
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
41
|
+
* Variables are lazily validated on access — if a required variable is
|
|
42
|
+
* missing or invalid, an error is thrown at the first access point.
|
|
43
|
+
* This is timing-safe with dotenv loading.
|
|
43
44
|
*
|
|
44
45
|
* @example
|
|
45
46
|
* ```typescript
|
|
46
47
|
* import env from '@/server/config/env.config';
|
|
47
48
|
*
|
|
48
|
-
*
|
|
49
|
-
* const
|
|
50
|
-
*
|
|
51
|
-
* // Get optional variable (returns undefined if missing)
|
|
52
|
-
* const redisUrl = env.get('REDIS_URL');
|
|
53
|
-
*
|
|
54
|
-
* // Get with default value
|
|
55
|
-
* const port = env.get('PORT') ?? 8790;
|
|
49
|
+
* const dbUrl = env.DATABASE_URL; // throws if required and missing
|
|
50
|
+
* const port = env.PORT ?? 8790; // optional with fallback
|
|
56
51
|
* ```
|
|
57
|
-
*/
|
|
58
|
-
export const env = createEnvRegistry(envSchema);
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Validate Environment Variables
|
|
62
52
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* or if there are configuration errors.
|
|
53
|
+
* For explicit upfront validation (e.g. in a CLI script), call
|
|
54
|
+
* `createEnvRegistry(envSchema).validateAll()` to get `{ errors, warnings }`.
|
|
66
55
|
*/
|
|
67
|
-
|
|
68
|
-
{
|
|
69
|
-
const validation = env.validate();
|
|
70
|
-
|
|
71
|
-
// Critical errors (missing required variables)
|
|
72
|
-
if (!validation.valid)
|
|
73
|
-
{
|
|
74
|
-
console.error('\n❌ Environment Validation Failed\n');
|
|
75
|
-
console.error('The following required environment variables are missing or invalid:\n');
|
|
76
|
-
|
|
77
|
-
validation.errors.forEach((error) =>
|
|
78
|
-
{
|
|
79
|
-
console.error(` • ${error.key}`);
|
|
80
|
-
console.error(` ${error.message}`);
|
|
81
|
-
|
|
82
|
-
if (error.suggestion)
|
|
83
|
-
{
|
|
84
|
-
console.error(` 💡 ${error.suggestion}`);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
console.error('');
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
console.error('Please check your .env files and ensure all required variables are set.');
|
|
91
|
-
console.error('See .env.example for reference.\n');
|
|
92
|
-
|
|
93
|
-
process.exit(1);
|
|
94
|
-
}
|
|
56
|
+
export const env = createEnvRegistry(envSchema).validate();
|
|
95
57
|
|
|
96
|
-
|
|
97
|
-
if (validation.warnings.length > 0)
|
|
98
|
-
{
|
|
99
|
-
console.warn('\n⚠️ Environment Warnings:\n');
|
|
100
|
-
|
|
101
|
-
validation.warnings.forEach((warning) =>
|
|
102
|
-
{
|
|
103
|
-
console.warn(` • ${warning.key}: ${warning.message}`);
|
|
104
|
-
|
|
105
|
-
if (warning.suggestion)
|
|
106
|
-
{
|
|
107
|
-
console.warn(` 💡 ${warning.suggestion}`);
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
console.warn('');
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Export the registry as default
|
|
117
|
-
*
|
|
118
|
-
* Import this in your application to access environment variables:
|
|
119
|
-
* ```typescript
|
|
120
|
-
* import env from '@/server/config/env.config';
|
|
121
|
-
* ```
|
|
122
|
-
*/
|
|
123
|
-
export default env;
|
|
58
|
+
export default env;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { examples } from './example.entity';
|
|
1
|
+
export { examples } from './example.entity';
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
/**
|
|
3
2
|
* Application Router
|
|
4
3
|
*
|
|
@@ -20,10 +19,10 @@ export const appRouter = defineRouter({
|
|
|
20
19
|
getExample,
|
|
21
20
|
createExample,
|
|
22
21
|
updateExample,
|
|
23
|
-
deleteExample
|
|
22
|
+
deleteExample,
|
|
24
23
|
});
|
|
25
24
|
|
|
26
25
|
/**
|
|
27
26
|
* Router type for client usage
|
|
28
27
|
*/
|
|
29
|
-
export type AppRouter = typeof appRouter;
|
|
28
|
+
export type AppRouter = typeof appRouter;
|
|
@@ -39,26 +39,16 @@ export const listExamples = route.get('/examples')
|
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* GET /examples/:id - Get single example
|
|
42
|
-
*
|
|
43
|
-
* This route requires authorization header for testing header validation
|
|
44
42
|
*/
|
|
45
43
|
export const getExample = route.get('/examples/:id')
|
|
46
44
|
.input({
|
|
47
|
-
headers: Type.Object({
|
|
48
|
-
test: Type.String({
|
|
49
|
-
description: 'Bearer token for authentication'
|
|
50
|
-
})
|
|
51
|
-
}),
|
|
52
45
|
params: Type.Object({
|
|
53
46
|
id: Type.String(),
|
|
54
47
|
}),
|
|
55
48
|
})
|
|
56
49
|
.handler(async (c) =>
|
|
57
50
|
{
|
|
58
|
-
const { params
|
|
59
|
-
|
|
60
|
-
// Log authorization header for testing
|
|
61
|
-
console.log('Authorization:', headers.test);
|
|
51
|
+
const { params } = await c.data();
|
|
62
52
|
|
|
63
53
|
const example = await exampleRepo.findById(params.id);
|
|
64
54
|
|
|
@@ -137,4 +127,4 @@ export const deleteExample = route.delete('/examples/:id')
|
|
|
137
127
|
success: true,
|
|
138
128
|
id: params.id,
|
|
139
129
|
};
|
|
140
|
-
});
|
|
130
|
+
});
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* Server Configuration
|
|
3
3
|
*/
|
|
4
4
|
import { defineServerConfig } from '@spfn/core/server';
|
|
5
|
-
import { appRouter } from
|
|
5
|
+
import { appRouter } from '@/server/router';
|
|
6
6
|
|
|
7
7
|
export default defineServerConfig()
|
|
8
8
|
.port(8790)
|
|
9
9
|
.host('0.0.0.0')
|
|
10
10
|
.routes(appRouter)
|
|
11
|
-
.build();
|
|
11
|
+
.build();
|
package/package.json
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spfn",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.60",
|
|
4
4
|
"description": "Superfunction CLI - Add SPFN to your Next.js project",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"spfn": "
|
|
7
|
+
"spfn": "bin/spfn.js"
|
|
8
8
|
},
|
|
9
9
|
"types": "./dist/index.d.ts",
|
|
10
10
|
"files": [
|
|
11
11
|
"dist",
|
|
12
12
|
"bin"
|
|
13
13
|
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsup && node scripts/copy-templates.js",
|
|
16
|
+
"test": "vitest run",
|
|
17
|
+
"prepack": "node -e \"require('fs').copyFileSync('../../LICENSE','LICENSE')\"",
|
|
18
|
+
"dev": "tsup --watch",
|
|
19
|
+
"type-check": "tsc --noEmit",
|
|
20
|
+
"publish:alpha": "npm publish --access public --tag alpha",
|
|
21
|
+
"publish:beta": "npm publish --access public --tag beta",
|
|
22
|
+
"publish:latest": "npm publish --access public"
|
|
23
|
+
},
|
|
14
24
|
"keywords": [
|
|
15
25
|
"spfn",
|
|
16
26
|
"cli",
|
|
@@ -27,26 +37,26 @@
|
|
|
27
37
|
"typescript",
|
|
28
38
|
"type-safe"
|
|
29
39
|
],
|
|
30
|
-
"author": "Ray Im <rayim@
|
|
40
|
+
"author": "Ray Im <rayim@fxy.global>",
|
|
31
41
|
"license": "MIT",
|
|
32
42
|
"repository": {
|
|
33
43
|
"type": "git",
|
|
34
|
-
"url": "https://github.com/
|
|
44
|
+
"url": "git+https://github.com/fxylabs/spfn.git",
|
|
35
45
|
"directory": "packages/cli"
|
|
36
46
|
},
|
|
37
47
|
"publishConfig": {
|
|
38
48
|
"access": "public",
|
|
39
|
-
"tag": "
|
|
40
|
-
"registry": "https://registry.npmjs.org"
|
|
49
|
+
"tag": "beta"
|
|
41
50
|
},
|
|
42
51
|
"bugs": {
|
|
43
|
-
"url": "https://github.com/
|
|
52
|
+
"url": "https://github.com/fxylabs/spfn/issues"
|
|
44
53
|
},
|
|
45
|
-
"homepage": "https://github.com/
|
|
54
|
+
"homepage": "https://github.com/fxylabs/spfn/tree/main/packages/cli#readme",
|
|
46
55
|
"engines": {
|
|
47
56
|
"node": ">=18.18.0"
|
|
48
57
|
},
|
|
49
58
|
"peerDependencies": {
|
|
59
|
+
"@spfn/core": ">=0.2.0-beta.54",
|
|
50
60
|
"typescript": "^5.3.0"
|
|
51
61
|
},
|
|
52
62
|
"peerDependenciesMeta": {
|
|
@@ -59,7 +69,8 @@
|
|
|
59
69
|
"chokidar": "^4.0.3",
|
|
60
70
|
"commander": "^11.1.0",
|
|
61
71
|
"dotenv": "^17.2.3",
|
|
62
|
-
"drizzle-
|
|
72
|
+
"drizzle-kit": "1.0.0-rc.4",
|
|
73
|
+
"drizzle-orm": "1.0.0-rc.4",
|
|
63
74
|
"execa": "^8.0.1",
|
|
64
75
|
"fs-extra": "^11.2.0",
|
|
65
76
|
"ora": "^7.0.1",
|
|
@@ -67,21 +78,21 @@
|
|
|
67
78
|
"postgres": "^3.4.0",
|
|
68
79
|
"prompts": "^2.4.2",
|
|
69
80
|
"tsup": "^8.5.0",
|
|
70
|
-
"
|
|
81
|
+
"yaml": "^2.5.0"
|
|
82
|
+
},
|
|
83
|
+
"optionalDependencies": {
|
|
84
|
+
"@napi-rs/keyring": "^1.1.0"
|
|
71
85
|
},
|
|
72
86
|
"devDependencies": {
|
|
87
|
+
"@electric-sql/pglite": "^0.5.4",
|
|
88
|
+
"@spfn/core": "workspace:*",
|
|
73
89
|
"@types/fs-extra": "^11.0.4",
|
|
74
90
|
"@types/node": "^20.11.0",
|
|
75
91
|
"@types/pg": "^8.10.9",
|
|
76
92
|
"@types/prompts": "^2.4.9",
|
|
77
93
|
"concurrently": "^9.2.1",
|
|
78
|
-
"drizzle-kit": "^0.31.6",
|
|
79
94
|
"tsx": "^4.20.6",
|
|
80
|
-
"typescript": "^5.3.3"
|
|
81
|
-
|
|
82
|
-
"scripts": {
|
|
83
|
-
"build": "tsup && node scripts/copy-templates.js",
|
|
84
|
-
"dev": "tsup --watch",
|
|
85
|
-
"type-check": "tsc --noEmit"
|
|
95
|
+
"typescript": "^5.3.3",
|
|
96
|
+
"vitest": "^4.0.6"
|
|
86
97
|
}
|
|
87
|
-
}
|
|
98
|
+
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* {{PASCAL_NAME}} Contracts
|
|
3
|
-
*
|
|
4
|
-
* Auto-generated by SPFN CLI
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { Type } from '@sinclair/typebox';
|
|
8
|
-
import { createContract } from '@spfn/core/route';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* GET /{{ENTITY_NAME}}
|
|
12
|
-
* List {{PASCAL_NAME}} items
|
|
13
|
-
*/
|
|
14
|
-
export const list{{PASCAL_NAME}}Contract = createContract({
|
|
15
|
-
method: 'GET',
|
|
16
|
-
path: '/{{ENTITY_NAME}}',
|
|
17
|
-
query: Type.Object({
|
|
18
|
-
limit: Type.Optional(Type.Number({ minimum: 1, maximum: 100 })),
|
|
19
|
-
offset: Type.Optional(Type.Number({ minimum: 0 })),
|
|
20
|
-
}),
|
|
21
|
-
responses: {
|
|
22
|
-
200: Type.Object({
|
|
23
|
-
items: Type.Array(
|
|
24
|
-
Type.Object({
|
|
25
|
-
id: Type.Number(),
|
|
26
|
-
name: Type.String(),
|
|
27
|
-
description: Type.Union([Type.String(), Type.Null()]),
|
|
28
|
-
createdAt: Type.String(),
|
|
29
|
-
updatedAt: Type.String(),
|
|
30
|
-
})
|
|
31
|
-
),
|
|
32
|
-
limit: Type.Number(),
|
|
33
|
-
offset: Type.Number(),
|
|
34
|
-
}),
|
|
35
|
-
},
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* POST /{{ENTITY_NAME}}
|
|
40
|
-
* Create new {{PASCAL_NAME}}
|
|
41
|
-
*/
|
|
42
|
-
export const create{{PASCAL_NAME}}Contract = createContract({
|
|
43
|
-
method: 'POST',
|
|
44
|
-
path: '/{{ENTITY_NAME}}',
|
|
45
|
-
body: Type.Object({
|
|
46
|
-
name: Type.String({ minLength: 1 }),
|
|
47
|
-
description: Type.Optional(Type.String()),
|
|
48
|
-
}),
|
|
49
|
-
responses: {
|
|
50
|
-
201: Type.Object({
|
|
51
|
-
id: Type.Number(),
|
|
52
|
-
name: Type.String(),
|
|
53
|
-
description: Type.Union([Type.String(), Type.Null()]),
|
|
54
|
-
createdAt: Type.String(),
|
|
55
|
-
updatedAt: Type.String(),
|
|
56
|
-
}),
|
|
57
|
-
},
|
|
58
|
-
});
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* {{PASCAL_NAME}} Entity
|
|
3
|
-
*
|
|
4
|
-
* Auto-generated by SPFN CLI
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { index, serial, text, timestamp } from 'drizzle-orm/pg-core';
|
|
8
|
-
import { {{SCHEMA_VAR_NAME}} } from './{{SCHEMA_FILE_NAME}}';
|
|
9
|
-
|
|
10
|
-
export const {{ENTITY_NAME}} = {{SCHEMA_VAR_NAME}}.table('{{TABLE_NAME}}', {
|
|
11
|
-
// Primary Key
|
|
12
|
-
id: serial('id').primaryKey(),
|
|
13
|
-
|
|
14
|
-
// Fields
|
|
15
|
-
name: text('name').notNull(),
|
|
16
|
-
description: text('description'),
|
|
17
|
-
|
|
18
|
-
// Timestamps
|
|
19
|
-
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
20
|
-
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
21
|
-
}, (table) => [
|
|
22
|
-
// Indexes
|
|
23
|
-
index('{{SCHEMA_NAME}}_{{TABLE_NAME}}_name_idx').on(table.name),
|
|
24
|
-
]);
|
|
25
|
-
|
|
26
|
-
// Type inference
|
|
27
|
-
export type {{PASCAL_NAME}} = typeof {{ENTITY_NAME}}.$inferSelect;
|
|
28
|
-
export type New{{PASCAL_NAME}} = typeof {{ENTITY_NAME}}.$inferInsert;
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* {{PASCAL_NAME}} Repository
|
|
3
|
-
*
|
|
4
|
-
* Auto-generated by SPFN CLI
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { findOne, findMany, create, update, deleteById } from '@spfn/core/db';
|
|
8
|
-
import { eq } from 'drizzle-orm';
|
|
9
|
-
import { {{ENTITY_NAME}} } from '@/entities';
|
|
10
|
-
import type { {{PASCAL_NAME}}, New{{PASCAL_NAME}} } from '@/entities';
|
|
11
|
-
|
|
12
|
-
export const {{REPO_NAME}} = {
|
|
13
|
-
/**
|
|
14
|
-
* Find {{PASCAL_NAME}} by ID
|
|
15
|
-
*/
|
|
16
|
-
findById: async (id: number): Promise<{{PASCAL_NAME}} | null> =>
|
|
17
|
-
{
|
|
18
|
-
return findOne({{ENTITY_NAME}}, eq({{ENTITY_NAME}}.id, id));
|
|
19
|
-
},
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Find all {{PASCAL_NAME}} items
|
|
23
|
-
*/
|
|
24
|
-
findMany: async (options?: { limit?: number; offset?: number }): Promise<{{PASCAL_NAME}}[]> =>
|
|
25
|
-
{
|
|
26
|
-
const { limit = 20, offset = 0 } = options || {};
|
|
27
|
-
return findMany({{ENTITY_NAME}}, {
|
|
28
|
-
limit,
|
|
29
|
-
offset,
|
|
30
|
-
});
|
|
31
|
-
},
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Create new {{PASCAL_NAME}}
|
|
35
|
-
*/
|
|
36
|
-
create: async (data: New{{PASCAL_NAME}}): Promise<{{PASCAL_NAME}}> =>
|
|
37
|
-
{
|
|
38
|
-
return create({{ENTITY_NAME}}, data);
|
|
39
|
-
},
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Update {{PASCAL_NAME}} by ID
|
|
43
|
-
*/
|
|
44
|
-
updateById: async (id: number, data: Partial<New{{PASCAL_NAME}}>): Promise<{{PASCAL_NAME}} | null> =>
|
|
45
|
-
{
|
|
46
|
-
return update({{ENTITY_NAME}}, eq({{ENTITY_NAME}}.id, id), data);
|
|
47
|
-
},
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Delete {{PASCAL_NAME}} by ID
|
|
51
|
-
*/
|
|
52
|
-
deleteById: async (id: number): Promise<void> =>
|
|
53
|
-
{
|
|
54
|
-
await deleteById({{ENTITY_NAME}}, id);
|
|
55
|
-
},
|
|
56
|
-
};
|