create-faas-app 8.0.0-beta.25 → 8.0.0-beta.27
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/README.md +73 -1
- package/dist/index.mjs +20 -19
- package/package.json +1 -1
- package/template/admin/.env.example +1 -0
- package/template/{antd → admin}/gitignore +1 -0
- package/template/admin/migrations/20250101000000_create_users.ts +12 -0
- package/template/{basic → admin}/package.json +13 -6
- package/template/admin/src/pages/home/api/auth/__tests__/me.test.ts +39 -0
- package/template/admin/src/pages/home/api/auth/me.api.ts +21 -0
- package/template/admin/src/pages/home/api/users/__tests__/create.test.ts +47 -0
- package/template/admin/src/pages/home/api/users/__tests__/detail.test.ts +39 -0
- package/template/admin/src/pages/home/api/users/__tests__/list.test.ts +31 -0
- package/template/admin/src/pages/home/api/users/__tests__/update.test.ts +51 -0
- package/template/admin/src/pages/home/api/users/create.api.ts +26 -0
- package/template/admin/src/pages/home/api/users/detail.api.ts +23 -0
- package/template/admin/src/pages/home/api/users/list.api.ts +22 -0
- package/template/admin/src/pages/home/api/users/update.api.ts +35 -0
- package/template/admin/src/pages/home/index.tsx +173 -0
- package/template/admin/src/plugins/auth.ts +25 -0
- package/template/admin/src/types/faasjs-auth.d.ts +8 -0
- package/template/admin/src/types/faasjs-pg.d.ts +10 -0
- package/template/admin/tsconfig.json +4 -0
- package/template/admin/vite.config.ts +12 -0
- package/template/{basic → minimal}/gitignore +1 -0
- package/template/{antd → minimal}/package.json +3 -4
- package/template/{antd → minimal}/src/pages/home/api/hello.api.ts +2 -1
- package/template/{basic → minimal}/src/pages/home/index.tsx +1 -1
- package/template/antd/src/pages/home/index.tsx +0 -79
- package/template/basic/src/pages/home/api/__tests__/hello.test.ts +0 -17
- package/template/basic/src/pages/home/api/hello.api.ts +0 -12
- package/template/basic/tsconfig.json +0 -4
- package/template/basic/vite.config.ts +0 -6
- /package/template/{antd → admin}/index.html +0 -0
- /package/template/{antd → admin}/server.ts +0 -0
- /package/template/{antd → admin}/src/faas.yaml +0 -0
- /package/template/{antd → admin}/src/main.tsx +0 -0
- /package/template/{basic → minimal}/index.html +0 -0
- /package/template/{basic → minimal}/server.ts +0 -0
- /package/template/{basic → minimal}/src/faas.yaml +0 -0
- /package/template/{basic → minimal}/src/main.tsx +0 -0
- /package/template/{antd → minimal}/src/pages/home/api/__tests__/hello.test.ts +0 -0
- /package/template/{basic → minimal}/src/react-client.ts +0 -0
- /package/template/{antd → minimal}/tsconfig.json +0 -0
- /package/template/{antd → minimal}/vite.config.ts +0 -0
package/README.md
CHANGED
|
@@ -1,5 +1,77 @@
|
|
|
1
1
|
# create-faas-app
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Create a new FaasJS app from a curated starter template.
|
|
4
|
+
|
|
5
|
+
FaasJS is optimized for database-driven React business applications. `create-faas-app` gives new projects a working starting point for the official path instead of asking every team to assemble React, API, database, testing, and UI conventions from scratch.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx create-faas-app --name my-faas-app
|
|
11
|
+
cd my-faas-app
|
|
12
|
+
npm run dev
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The default template is `admin`, which demonstrates the curated React + Ant Design + PostgreSQL path.
|
|
16
|
+
|
|
17
|
+
## Templates
|
|
18
|
+
|
|
19
|
+
### `admin`
|
|
20
|
+
|
|
21
|
+
Use `admin` for the golden-path FaasJS starter.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx create-faas-app --name my-admin --template admin
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
It includes:
|
|
28
|
+
|
|
29
|
+
- React app structure powered by Vite Plus
|
|
30
|
+
- `@faasjs/ant-design` and Ant Design for business UI
|
|
31
|
+
- `defineApi` endpoints for typed backend APIs
|
|
32
|
+
- a copyable users slice with create, list, detail, update, migration, table types, and API tests
|
|
33
|
+
- `@faasjs/pg` for PostgreSQL access and migrations
|
|
34
|
+
- `@faasjs/pg-dev` for pg-dev-powered tests
|
|
35
|
+
- `.env.example` for local database configuration
|
|
36
|
+
- type declarations for PostgreSQL table inference
|
|
37
|
+
|
|
38
|
+
This is the best starting point for admin panels, internal tools, SaaS dashboards, and other database-driven business applications.
|
|
39
|
+
|
|
40
|
+
### `minimal`
|
|
41
|
+
|
|
42
|
+
Use `minimal` when you want a smaller React starter without the database and Ant Design stack.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npx create-faas-app --name my-minimal-app --template minimal
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
It is useful for learning the core FaasJS runtime, building API-only or BFF-style projects, or adding a custom UI/database path intentionally.
|
|
49
|
+
|
|
50
|
+
## Recommended Path
|
|
51
|
+
|
|
52
|
+
Start with `admin` unless you have a specific reason not to. It shows how FaasJS expects complete application slices to fit together:
|
|
53
|
+
|
|
54
|
+
- UI pages call typed APIs
|
|
55
|
+
- APIs validate inputs at system boundaries
|
|
56
|
+
- APIs use PostgreSQL through the shared database workflow
|
|
57
|
+
- migrations and table types keep data contracts explicit
|
|
58
|
+
- tests cover the API and database behavior
|
|
59
|
+
|
|
60
|
+
FaasJS allows teams to replace parts of the stack, but the templates, docs, and examples optimize this curated path first.
|
|
61
|
+
|
|
62
|
+
## Auth And Permissions
|
|
63
|
+
|
|
64
|
+
Authentication and permissions are intentionally not built into FaasJS core because production auth requirements vary by product.
|
|
65
|
+
|
|
66
|
+
The admin starter includes a small auth plugin demo. Treat it as a plugin pattern that shows how to inject current-user context, protect APIs, and model project-specific permissions. It is not a mandatory framework auth system.
|
|
67
|
+
|
|
68
|
+
## Next Steps
|
|
69
|
+
|
|
70
|
+
- Read the FaasJS guide at <https://faasjs.com/guide/>.
|
|
71
|
+
- Review the root README for the project direction and package overview.
|
|
72
|
+
- Explore runnable templates in <https://github.com/faasjs/faasjs/tree/main/templates>.
|
|
73
|
+
- Use the admin starter users slice as the reference for complete UI/API/database/test examples.
|
|
74
|
+
|
|
75
|
+
## API Docs
|
|
4
76
|
|
|
5
77
|
- [main](functions/main.md)
|
package/dist/index.mjs
CHANGED
|
@@ -6,12 +6,13 @@ import { dirname, join } from "node:path";
|
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import enquirer from "enquirer";
|
|
8
8
|
//#region package.json
|
|
9
|
-
var version = "8.0.0-beta.
|
|
9
|
+
var version = "8.0.0-beta.26";
|
|
10
10
|
//#endregion
|
|
11
|
-
//#region src/action.ts
|
|
11
|
+
//#region src/action/index.ts
|
|
12
12
|
const prompt = enquirer.prompt;
|
|
13
13
|
const validateName = (input) => Validator.name(input);
|
|
14
|
-
const templateRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "template");
|
|
14
|
+
const templateRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "template");
|
|
15
|
+
const ignoredTemplateEntries = new Set(["node_modules"]);
|
|
15
16
|
const Validator = { name(input) {
|
|
16
17
|
const match = /^[a-z0-9-_]+$/i.test(input) ? true : "Must be a-z, 0-9 or -_";
|
|
17
18
|
if (match !== true) return match;
|
|
@@ -21,7 +22,7 @@ const Validator = { name(input) {
|
|
|
21
22
|
function getTemplateNames() {
|
|
22
23
|
return readdirSync(templateRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
23
24
|
}
|
|
24
|
-
function resolveTemplateName(template = "
|
|
25
|
+
function resolveTemplateName(template = "admin") {
|
|
25
26
|
const templates = getTemplateNames();
|
|
26
27
|
if (templates.includes(template)) return template;
|
|
27
28
|
throw new Error(`Unknown template "${template}". Available templates: ${templates.join(", ")}`);
|
|
@@ -35,6 +36,7 @@ function generateSessionSecret() {
|
|
|
35
36
|
function copyTemplateDirectory(sourcePath, targetPath, replacements) {
|
|
36
37
|
mkdirSync(targetPath, { recursive: true });
|
|
37
38
|
for (const entry of readdirSync(sourcePath, { withFileTypes: true })) {
|
|
39
|
+
if (ignoredTemplateEntries.has(entry.name)) continue;
|
|
38
40
|
const nextSourcePath = join(sourcePath, entry.name);
|
|
39
41
|
const nextTargetPath = join(targetPath, entry.name === "gitignore" ? ".gitignore" : entry.name);
|
|
40
42
|
if (entry.isDirectory()) {
|
|
@@ -53,14 +55,14 @@ function scaffold(rootPath, replacements, templateName) {
|
|
|
53
55
|
*
|
|
54
56
|
* @param {object} [options] - Optional CLI arguments used to choose the project name and template.
|
|
55
57
|
* @param {string} [options.name] - Target folder name for the generated app.
|
|
56
|
-
* @param {string} [options.template] - Template name such as `
|
|
58
|
+
* @param {string} [options.template] - Template name such as `admin` or `minimal`.
|
|
57
59
|
* @returns {Promise<void>} Resolves after the project is generated and its test command finishes.
|
|
58
60
|
* @throws {Error} When the selected template is unknown.
|
|
59
61
|
* @example
|
|
60
62
|
* ```ts
|
|
61
63
|
* await action({
|
|
62
64
|
* name: 'faasjs-demo',
|
|
63
|
-
* template: '
|
|
65
|
+
* template: 'admin',
|
|
64
66
|
* })
|
|
65
67
|
* ```
|
|
66
68
|
*/
|
|
@@ -75,14 +77,12 @@ async function action(options = {}) {
|
|
|
75
77
|
validate: validateName
|
|
76
78
|
}).then((res) => res.value);
|
|
77
79
|
if (!answers.name) return;
|
|
78
|
-
const runtime = process.versions.bun ? "bun" : "npm";
|
|
79
80
|
scaffold(answers.name, {
|
|
80
81
|
name: answers.name,
|
|
81
82
|
secret: generateSessionSecret()
|
|
82
83
|
}, templateName);
|
|
83
|
-
execSync(`cd ${answers.name} &&
|
|
84
|
-
|
|
85
|
-
else execSync(`cd ${answers.name} && npm run test`, { stdio: "inherit" });
|
|
84
|
+
execSync(`cd ${answers.name} && npm install`, { stdio: "inherit" });
|
|
85
|
+
execSync(`cd ${answers.name} && npm run test`, { stdio: "inherit" });
|
|
86
86
|
}
|
|
87
87
|
/**
|
|
88
88
|
* Register the `create-faas-app` command on a Commander program.
|
|
@@ -97,12 +97,17 @@ async function action(options = {}) {
|
|
|
97
97
|
* ```
|
|
98
98
|
*/
|
|
99
99
|
function registerCreateFaasApp(program) {
|
|
100
|
-
program.description("Create a new
|
|
100
|
+
program.description("Create a new FaasJS app").on("--help", () => console.log(`Examples:
|
|
101
101
|
npx create-faas-app --name faasjs
|
|
102
|
-
npx create-faas-app --name faasjs-admin --template
|
|
102
|
+
npx create-faas-app --name faasjs-admin --template admin
|
|
103
|
+
npx create-faas-app --name faasjs-minimal --template minimal
|
|
103
104
|
|
|
104
105
|
Templates:
|
|
105
|
-
|
|
106
|
+
admin: recommended React + Ant Design + PostgreSQL starter
|
|
107
|
+
minimal: lighter React starter
|
|
108
|
+
|
|
109
|
+
Available:
|
|
110
|
+
${getTemplateNames().join(", ")}`)).option("--name <name>", "Project name").option("--template <template>", "Template name", "admin").action(action);
|
|
106
111
|
}
|
|
107
112
|
//#endregion
|
|
108
113
|
//#region src/index.ts
|
|
@@ -115,13 +120,9 @@ ${getTemplateNames().join(", ")}`)).option("--name <name>", "Project name").opti
|
|
|
115
120
|
* ## Usage
|
|
116
121
|
*
|
|
117
122
|
* ```bash
|
|
118
|
-
* # use npm
|
|
119
123
|
* npx create-faas-app --name faasjs
|
|
120
|
-
* npx create-faas-app --name faasjs-admin --template
|
|
121
|
-
*
|
|
122
|
-
* # use bun
|
|
123
|
-
* bunx create-faas-app --name faasjs
|
|
124
|
-
* bunx create-faas-app --name faasjs-admin --template antd
|
|
124
|
+
* npx create-faas-app --name faasjs-admin --template admin
|
|
125
|
+
* npx create-faas-app --name faasjs-minimal --template minimal
|
|
125
126
|
* ```
|
|
126
127
|
*/
|
|
127
128
|
const commander = new Command();
|
package/package.json
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/{{name}}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SchemaBuilder } from '@faasjs/pg'
|
|
2
|
+
|
|
3
|
+
export function up(builder: SchemaBuilder) {
|
|
4
|
+
builder.createTable('users', (table) => {
|
|
5
|
+
table.specificType('id', 'serial').primary()
|
|
6
|
+
table.string('name')
|
|
7
|
+
})
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function down(builder: SchemaBuilder) {
|
|
11
|
+
builder.dropTable('users')
|
|
12
|
+
}
|
|
@@ -7,14 +7,21 @@
|
|
|
7
7
|
"dev": "vp dev",
|
|
8
8
|
"build": "vp build",
|
|
9
9
|
"start": "node --import @faasjs/node-utils/register-hooks server.ts",
|
|
10
|
-
"test": "vp test"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
"
|
|
10
|
+
"test": "vp test",
|
|
11
|
+
"db:new": "faasjs-pg new",
|
|
12
|
+
"db:status": "faasjs-pg status",
|
|
13
|
+
"db:migrate": "faasjs-pg migrate",
|
|
14
|
+
"db:up": "faasjs-pg up",
|
|
15
|
+
"db:down": "faasjs-pg down"
|
|
15
16
|
},
|
|
16
17
|
"devDependencies": {
|
|
17
|
-
"@faasjs/dev": "*"
|
|
18
|
+
"@faasjs/dev": "*",
|
|
19
|
+
"@faasjs/pg-dev": "*"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"@faasjs/ant-design": "*",
|
|
23
|
+
"@faasjs/core": "*",
|
|
24
|
+
"@faasjs/pg": "*"
|
|
18
25
|
},
|
|
19
26
|
"overrides": {
|
|
20
27
|
"vite": "npm:@voidzero-dev/vite-plus-core",
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { testApi } from '@faasjs/dev'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import api from '../me.api'
|
|
5
|
+
|
|
6
|
+
describe('pages/home/api/auth/me', () => {
|
|
7
|
+
it('returns the current user from the auth plugin demo', async () => {
|
|
8
|
+
const handler = testApi(api)
|
|
9
|
+
|
|
10
|
+
const { statusCode, data } = await handler(
|
|
11
|
+
{},
|
|
12
|
+
{
|
|
13
|
+
headers: {
|
|
14
|
+
authorization: 'Bearer demo-admin',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
expect(statusCode).toEqual(200)
|
|
20
|
+
expect(data).toEqual({
|
|
21
|
+
current_user: {
|
|
22
|
+
id: 1,
|
|
23
|
+
name: 'Demo Admin',
|
|
24
|
+
role: 'admin',
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('rejects requests without the demo token', async () => {
|
|
30
|
+
const handler = testApi(api)
|
|
31
|
+
|
|
32
|
+
const { statusCode, error } = await handler()
|
|
33
|
+
|
|
34
|
+
expect(statusCode).toEqual(401)
|
|
35
|
+
expect(error).toEqual({
|
|
36
|
+
message: 'Missing demo auth token',
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineApi, HttpError } from '@faasjs/core'
|
|
2
|
+
|
|
3
|
+
import { AuthPlugin } from '../../../../plugins/auth'
|
|
4
|
+
|
|
5
|
+
const api = defineApi({
|
|
6
|
+
async handler({ current_user }) {
|
|
7
|
+
if (!current_user)
|
|
8
|
+
throw new HttpError({
|
|
9
|
+
statusCode: 401,
|
|
10
|
+
message: 'Missing demo auth token',
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
current_user,
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
api.plugins.unshift(new AuthPlugin())
|
|
20
|
+
|
|
21
|
+
export default api
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { testApi } from '@faasjs/dev'
|
|
2
|
+
import { getClient } from '@faasjs/pg'
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import api from '../create.api'
|
|
6
|
+
|
|
7
|
+
describe('pages/home/api/users/create', () => {
|
|
8
|
+
it('creates a user with the shared pg bootstrap', async () => {
|
|
9
|
+
const handler = testApi(api)
|
|
10
|
+
|
|
11
|
+
const { statusCode, data } = await handler({ name: 'world' })
|
|
12
|
+
|
|
13
|
+
expect(statusCode).toEqual(200)
|
|
14
|
+
expect(data).toEqual({
|
|
15
|
+
message: 'Created user #1',
|
|
16
|
+
total: 1,
|
|
17
|
+
user: {
|
|
18
|
+
id: 1,
|
|
19
|
+
name: 'world',
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const client = await getClient()
|
|
24
|
+
|
|
25
|
+
await expect(client.query('users').orderBy('id', 'ASC')).resolves.toEqual([
|
|
26
|
+
{
|
|
27
|
+
id: 1,
|
|
28
|
+
name: 'world',
|
|
29
|
+
},
|
|
30
|
+
])
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('uses the default name when params.name is missing', async () => {
|
|
34
|
+
const handler = testApi(api)
|
|
35
|
+
|
|
36
|
+
const { data } = await handler({})
|
|
37
|
+
|
|
38
|
+
expect(data).toEqual({
|
|
39
|
+
message: 'Created user #1',
|
|
40
|
+
total: 1,
|
|
41
|
+
user: {
|
|
42
|
+
id: 1,
|
|
43
|
+
name: 'FaasJS',
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { testApi } from '@faasjs/dev'
|
|
2
|
+
import { getClient } from '@faasjs/pg'
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import api from '../detail.api'
|
|
6
|
+
|
|
7
|
+
describe('pages/home/api/users/detail', () => {
|
|
8
|
+
it('returns one user', async () => {
|
|
9
|
+
const client = await getClient()
|
|
10
|
+
const [created] = await client.query('users').insert(
|
|
11
|
+
{
|
|
12
|
+
name: 'Ada',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
returning: ['id'],
|
|
16
|
+
},
|
|
17
|
+
)
|
|
18
|
+
const handler = testApi(api)
|
|
19
|
+
const { statusCode, data } = await handler({ id: created.id })
|
|
20
|
+
|
|
21
|
+
expect(statusCode).toEqual(200)
|
|
22
|
+
expect(data).toEqual({
|
|
23
|
+
user: {
|
|
24
|
+
id: created.id,
|
|
25
|
+
name: 'Ada',
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('returns 404 when the user is missing', async () => {
|
|
31
|
+
const handler = testApi(api)
|
|
32
|
+
const { statusCode, error } = await handler({ id: 404 })
|
|
33
|
+
|
|
34
|
+
expect(statusCode).toEqual(404)
|
|
35
|
+
expect(error).toEqual({
|
|
36
|
+
message: 'User not found',
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { testApi } from '@faasjs/dev'
|
|
2
|
+
import { getClient } from '@faasjs/pg'
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import api from '../list.api'
|
|
6
|
+
|
|
7
|
+
describe('pages/home/api/users/list', () => {
|
|
8
|
+
it('lists users with total count', async () => {
|
|
9
|
+
const client = await getClient()
|
|
10
|
+
|
|
11
|
+
await client.query('users').insert([{ name: 'Ada' }, { name: 'Grace' }])
|
|
12
|
+
|
|
13
|
+
const handler = testApi(api)
|
|
14
|
+
const { statusCode, data } = await handler({ limit: 10 })
|
|
15
|
+
|
|
16
|
+
expect(statusCode).toEqual(200)
|
|
17
|
+
expect(data).toEqual({
|
|
18
|
+
total: 2,
|
|
19
|
+
users: [
|
|
20
|
+
{
|
|
21
|
+
id: 2,
|
|
22
|
+
name: 'Grace',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 1,
|
|
26
|
+
name: 'Ada',
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { testApi } from '@faasjs/dev'
|
|
2
|
+
import { getClient } from '@faasjs/pg'
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import api from '../update.api'
|
|
6
|
+
|
|
7
|
+
describe('pages/home/api/users/update', () => {
|
|
8
|
+
it('updates one user', async () => {
|
|
9
|
+
const client = await getClient()
|
|
10
|
+
const [created] = await client.query('users').insert(
|
|
11
|
+
{
|
|
12
|
+
name: 'Ada',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
returning: ['id'],
|
|
16
|
+
},
|
|
17
|
+
)
|
|
18
|
+
const handler = testApi(api)
|
|
19
|
+
const { statusCode, data } = await handler({
|
|
20
|
+
id: created.id,
|
|
21
|
+
name: 'Ada Lovelace',
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
expect(statusCode).toEqual(200)
|
|
25
|
+
expect(data).toEqual({
|
|
26
|
+
message: `Updated user #${created.id}`,
|
|
27
|
+
user: {
|
|
28
|
+
id: created.id,
|
|
29
|
+
name: 'Ada Lovelace',
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
await expect(client.query('users').where('id', created.id).first()).resolves.toEqual({
|
|
34
|
+
id: created.id,
|
|
35
|
+
name: 'Ada Lovelace',
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('returns 404 when the user is missing', async () => {
|
|
40
|
+
const handler = testApi(api)
|
|
41
|
+
const { statusCode, error } = await handler({
|
|
42
|
+
id: 404,
|
|
43
|
+
name: 'Missing',
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
expect(statusCode).toEqual(404)
|
|
47
|
+
expect(error).toEqual({
|
|
48
|
+
message: 'User not found',
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { defineApi } from '@faasjs/core'
|
|
2
|
+
import { getClient } from '@faasjs/pg'
|
|
3
|
+
import * as z from 'zod'
|
|
4
|
+
|
|
5
|
+
export default defineApi({
|
|
6
|
+
schema: z.object({
|
|
7
|
+
name: z.string().min(1).optional(),
|
|
8
|
+
}),
|
|
9
|
+
async handler({ params }) {
|
|
10
|
+
const client = await getClient()
|
|
11
|
+
const [user] = await client.query('users').insert(
|
|
12
|
+
{
|
|
13
|
+
name: params.name || 'FaasJS',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
returning: ['id', 'name'],
|
|
17
|
+
},
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
message: `Created user #${user.id}`,
|
|
22
|
+
total: await client.query('users').count(),
|
|
23
|
+
user,
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { defineApi, HttpError } from '@faasjs/core'
|
|
2
|
+
import { getClient } from '@faasjs/pg'
|
|
3
|
+
import * as z from 'zod'
|
|
4
|
+
|
|
5
|
+
export default defineApi({
|
|
6
|
+
schema: z.object({
|
|
7
|
+
id: z.number().int().positive(),
|
|
8
|
+
}),
|
|
9
|
+
async handler({ params }) {
|
|
10
|
+
const client = await getClient()
|
|
11
|
+
const user = await client.query('users').select('id', 'name').where('id', params.id).first()
|
|
12
|
+
|
|
13
|
+
if (!user)
|
|
14
|
+
throw new HttpError({
|
|
15
|
+
statusCode: 404,
|
|
16
|
+
message: 'User not found',
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
user,
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { defineApi } from '@faasjs/core'
|
|
2
|
+
import { getClient } from '@faasjs/pg'
|
|
3
|
+
import * as z from 'zod'
|
|
4
|
+
|
|
5
|
+
export default defineApi({
|
|
6
|
+
schema: z.object({
|
|
7
|
+
limit: z.number().int().positive().max(50).default(20),
|
|
8
|
+
}),
|
|
9
|
+
async handler({ params }) {
|
|
10
|
+
const client = await getClient()
|
|
11
|
+
const users = await client
|
|
12
|
+
.query('users')
|
|
13
|
+
.select('id', 'name')
|
|
14
|
+
.orderBy('id', 'DESC')
|
|
15
|
+
.limit(params.limit)
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
total: await client.query('users').count(),
|
|
19
|
+
users,
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { defineApi, HttpError } from '@faasjs/core'
|
|
2
|
+
import { getClient } from '@faasjs/pg'
|
|
3
|
+
import * as z from 'zod'
|
|
4
|
+
|
|
5
|
+
export default defineApi({
|
|
6
|
+
schema: z.object({
|
|
7
|
+
id: z.number().int().positive(),
|
|
8
|
+
name: z.string().min(1),
|
|
9
|
+
}),
|
|
10
|
+
async handler({ params }) {
|
|
11
|
+
const client = await getClient()
|
|
12
|
+
const [user] = await client
|
|
13
|
+
.query('users')
|
|
14
|
+
.where('id', params.id)
|
|
15
|
+
.update(
|
|
16
|
+
{
|
|
17
|
+
name: params.name,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
returning: ['id', 'name'],
|
|
21
|
+
},
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
if (!user)
|
|
25
|
+
throw new HttpError({
|
|
26
|
+
statusCode: 404,
|
|
27
|
+
message: 'User not found',
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
message: `Updated user #${user.id}`,
|
|
32
|
+
user,
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
})
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { faas, useApp } from '@faasjs/ant-design'
|
|
2
|
+
import { Button, Card, Input, Space, Table, Typography } from 'antd'
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
|
|
5
|
+
type CurrentUserResponse = {
|
|
6
|
+
current_user?: {
|
|
7
|
+
id: number
|
|
8
|
+
name: string
|
|
9
|
+
role: string
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type UserRecord = {
|
|
14
|
+
id: number
|
|
15
|
+
name: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type ListUsersResponse = {
|
|
19
|
+
total?: number
|
|
20
|
+
users?: UserRecord[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type CreateUserResponse = {
|
|
24
|
+
message?: string
|
|
25
|
+
total?: number
|
|
26
|
+
user?: UserRecord
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default function HomePage() {
|
|
30
|
+
const app = useApp()
|
|
31
|
+
const [name, setName] = useState('FaasJS')
|
|
32
|
+
const [messageText, setMessageText] = useState('Create your first user through the FaasJS API')
|
|
33
|
+
const [loading, setLoading] = useState(false)
|
|
34
|
+
const [authLoading, setAuthLoading] = useState(false)
|
|
35
|
+
const [users, setUsers] = useState<UserRecord[]>([])
|
|
36
|
+
|
|
37
|
+
const refreshUsers = async () => {
|
|
38
|
+
const response = await faas('/pages/home/api/users/list', {
|
|
39
|
+
limit: 10,
|
|
40
|
+
})
|
|
41
|
+
const data = (response.data as ListUsersResponse | undefined) || undefined
|
|
42
|
+
|
|
43
|
+
setUsers(data?.users || [])
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const callApi = async () => {
|
|
47
|
+
setLoading(true)
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const response = await faas('/pages/home/api/users/create', {
|
|
51
|
+
name: name.trim() || undefined,
|
|
52
|
+
})
|
|
53
|
+
const data = (response.data as CreateUserResponse | undefined) || undefined
|
|
54
|
+
const nextMessage =
|
|
55
|
+
data?.user && typeof data.total === 'number'
|
|
56
|
+
? `Created ${data.user.name} (#${data.user.id}). Total users: ${data.total}`
|
|
57
|
+
: data?.message || 'Empty response'
|
|
58
|
+
|
|
59
|
+
setMessageText(nextMessage)
|
|
60
|
+
setUsers((current) => (data?.user ? [data.user, ...current].slice(0, 10) : current))
|
|
61
|
+
app.message.success('User saved to PostgreSQL')
|
|
62
|
+
} catch (error: unknown) {
|
|
63
|
+
const errorMessage = error instanceof Error ? error.message : 'Request failed'
|
|
64
|
+
|
|
65
|
+
setMessageText(errorMessage)
|
|
66
|
+
app.notification.error({
|
|
67
|
+
message: 'API call failed',
|
|
68
|
+
description: errorMessage,
|
|
69
|
+
})
|
|
70
|
+
} finally {
|
|
71
|
+
setLoading(false)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const callAuthDemo = async () => {
|
|
76
|
+
setAuthLoading(true)
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const response = await faas(
|
|
80
|
+
'/pages/home/api/auth/me',
|
|
81
|
+
{},
|
|
82
|
+
{
|
|
83
|
+
headers: {
|
|
84
|
+
authorization: 'Bearer demo-admin',
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
)
|
|
88
|
+
const data = (response.data as CurrentUserResponse | undefined) || undefined
|
|
89
|
+
|
|
90
|
+
setMessageText(`Auth plugin injected current user: ${data?.current_user?.name || 'unknown'}`)
|
|
91
|
+
app.message.success('Auth plugin demo loaded current_user')
|
|
92
|
+
} catch (error: unknown) {
|
|
93
|
+
const errorMessage = error instanceof Error ? error.message : 'Auth demo failed'
|
|
94
|
+
|
|
95
|
+
setMessageText(errorMessage)
|
|
96
|
+
app.notification.error({
|
|
97
|
+
message: 'Auth demo failed',
|
|
98
|
+
description: errorMessage,
|
|
99
|
+
})
|
|
100
|
+
} finally {
|
|
101
|
+
setAuthLoading(false)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<div
|
|
107
|
+
style={{
|
|
108
|
+
minHeight: '100vh',
|
|
109
|
+
display: 'grid',
|
|
110
|
+
placeItems: 'center',
|
|
111
|
+
padding: 24,
|
|
112
|
+
background: 'linear-gradient(135deg, #f5f7fa 0%, #e4ecfb 100%)',
|
|
113
|
+
}}
|
|
114
|
+
>
|
|
115
|
+
<Card
|
|
116
|
+
style={{
|
|
117
|
+
width: '100%',
|
|
118
|
+
maxWidth: 640,
|
|
119
|
+
boxShadow: '0 20px 45px rgba(15, 23, 42, 0.08)',
|
|
120
|
+
}}
|
|
121
|
+
>
|
|
122
|
+
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
|
123
|
+
<div>
|
|
124
|
+
<Typography.Title level={2}>FaasJS Admin App</Typography.Title>
|
|
125
|
+
<Typography.Paragraph type="secondary">
|
|
126
|
+
This starter follows the curated FaasJS path: React, Ant Design, PostgreSQL,
|
|
127
|
+
pg-dev-powered tests, and a simple auth plugin demo.
|
|
128
|
+
</Typography.Paragraph>
|
|
129
|
+
<Typography.Paragraph type="secondary">
|
|
130
|
+
Set <code>DATABASE_URL</code> from <code>.env.example</code> and run{' '}
|
|
131
|
+
<code>npm run db:migrate</code> before using the page in development.
|
|
132
|
+
</Typography.Paragraph>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<Input
|
|
136
|
+
value={name}
|
|
137
|
+
onChange={(event) => setName(event.target.value)}
|
|
138
|
+
placeholder="Who should the admin app create?"
|
|
139
|
+
/>
|
|
140
|
+
|
|
141
|
+
<Space wrap>
|
|
142
|
+
<Button onClick={refreshUsers}>Load users slice</Button>
|
|
143
|
+
<Button type="primary" loading={loading} onClick={callApi}>
|
|
144
|
+
Create /pages/home/api/users/create
|
|
145
|
+
</Button>
|
|
146
|
+
<Button loading={authLoading} onClick={callAuthDemo}>
|
|
147
|
+
Call auth plugin demo
|
|
148
|
+
</Button>
|
|
149
|
+
</Space>
|
|
150
|
+
|
|
151
|
+
<Table<UserRecord>
|
|
152
|
+
rowKey="id"
|
|
153
|
+
size="small"
|
|
154
|
+
pagination={false}
|
|
155
|
+
dataSource={users}
|
|
156
|
+
columns={[
|
|
157
|
+
{
|
|
158
|
+
title: 'ID',
|
|
159
|
+
dataIndex: 'id',
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
title: 'Name',
|
|
163
|
+
dataIndex: 'name',
|
|
164
|
+
},
|
|
165
|
+
]}
|
|
166
|
+
/>
|
|
167
|
+
|
|
168
|
+
<Typography.Paragraph style={{ marginBottom: 0 }}>{messageText}</Typography.Paragraph>
|
|
169
|
+
</Space>
|
|
170
|
+
</Card>
|
|
171
|
+
</div>
|
|
172
|
+
)
|
|
173
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { InvokeData, Next, Plugin } from '@faasjs/core'
|
|
2
|
+
|
|
3
|
+
export type CurrentUser = {
|
|
4
|
+
id: number
|
|
5
|
+
name: string
|
|
6
|
+
role: 'admin'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class AuthPlugin implements Plugin {
|
|
10
|
+
public readonly name = 'auth'
|
|
11
|
+
public readonly type = 'auth'
|
|
12
|
+
|
|
13
|
+
public async onInvoke(data: InvokeData, next: Next): Promise<void> {
|
|
14
|
+
const token = data.event?.headers?.authorization || data.event?.headers?.Authorization
|
|
15
|
+
|
|
16
|
+
if (token === 'Bearer demo-admin')
|
|
17
|
+
data.current_user = {
|
|
18
|
+
id: 1,
|
|
19
|
+
name: 'Demo Admin',
|
|
20
|
+
role: 'admin',
|
|
21
|
+
} satisfies CurrentUser
|
|
22
|
+
|
|
23
|
+
await next()
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { viteConfig } from '@faasjs/dev'
|
|
2
|
+
import { PgVitestPlugin } from '@faasjs/pg-dev'
|
|
3
|
+
import { defineConfig } from 'vite-plus'
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
...viteConfig,
|
|
7
|
+
plugins: [...viteConfig.plugins, PgVitestPlugin()],
|
|
8
|
+
test: {
|
|
9
|
+
fileParallelism: false,
|
|
10
|
+
testTimeout: 30_000,
|
|
11
|
+
},
|
|
12
|
+
})
|
|
@@ -9,13 +9,12 @@
|
|
|
9
9
|
"start": "node --import @faasjs/node-utils/register-hooks server.ts",
|
|
10
10
|
"test": "vp test"
|
|
11
11
|
},
|
|
12
|
-
"dependencies": {
|
|
13
|
-
"@faasjs/ant-design": "*",
|
|
14
|
-
"@faasjs/core": "*"
|
|
15
|
-
},
|
|
16
12
|
"devDependencies": {
|
|
17
13
|
"@faasjs/dev": "*"
|
|
18
14
|
},
|
|
15
|
+
"peerDependencies": {
|
|
16
|
+
"@faasjs/core": "*"
|
|
17
|
+
},
|
|
19
18
|
"overrides": {
|
|
20
19
|
"vite": "npm:@voidzero-dev/vite-plus-core",
|
|
21
20
|
"vitest": "npm:@voidzero-dev/vite-plus-test"
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { faas, useApp } from '@faasjs/ant-design'
|
|
2
|
-
import { Button, Card, Input, Space, Typography } from 'antd'
|
|
3
|
-
import { useState } from 'react'
|
|
4
|
-
|
|
5
|
-
type HelloResponse = {
|
|
6
|
-
message?: string
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export default function HomePage() {
|
|
10
|
-
const app = useApp()
|
|
11
|
-
const [name, setName] = useState('FaasJS')
|
|
12
|
-
const [messageText, setMessageText] = useState('Click button to call API')
|
|
13
|
-
const [loading, setLoading] = useState(false)
|
|
14
|
-
|
|
15
|
-
const callApi = async () => {
|
|
16
|
-
setLoading(true)
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
const response = await faas('/pages/home/api/hello', {
|
|
20
|
-
name: name.trim() || undefined,
|
|
21
|
-
})
|
|
22
|
-
const nextMessage = (response.data as HelloResponse | undefined)?.message || 'Empty response'
|
|
23
|
-
|
|
24
|
-
setMessageText(nextMessage)
|
|
25
|
-
app.message.success('API call succeeded')
|
|
26
|
-
} catch (error: unknown) {
|
|
27
|
-
const errorMessage = error instanceof Error ? error.message : 'Request failed'
|
|
28
|
-
|
|
29
|
-
setMessageText(errorMessage)
|
|
30
|
-
app.notification.error({
|
|
31
|
-
message: 'API call failed',
|
|
32
|
-
description: errorMessage,
|
|
33
|
-
})
|
|
34
|
-
} finally {
|
|
35
|
-
setLoading(false)
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return (
|
|
40
|
-
<div
|
|
41
|
-
style={{
|
|
42
|
-
minHeight: '100vh',
|
|
43
|
-
display: 'grid',
|
|
44
|
-
placeItems: 'center',
|
|
45
|
-
padding: 24,
|
|
46
|
-
background: 'linear-gradient(135deg, #f5f7fa 0%, #e4ecfb 100%)',
|
|
47
|
-
}}
|
|
48
|
-
>
|
|
49
|
-
<Card
|
|
50
|
-
style={{
|
|
51
|
-
width: '100%',
|
|
52
|
-
maxWidth: 560,
|
|
53
|
-
boxShadow: '0 20px 45px rgba(15, 23, 42, 0.08)',
|
|
54
|
-
}}
|
|
55
|
-
>
|
|
56
|
-
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
|
57
|
-
<div>
|
|
58
|
-
<Typography.Title level={2}>FaasJS Ant Design App</Typography.Title>
|
|
59
|
-
<Typography.Paragraph type="secondary">
|
|
60
|
-
Call a FaasJS API through the Ant Design app shell.
|
|
61
|
-
</Typography.Paragraph>
|
|
62
|
-
</div>
|
|
63
|
-
|
|
64
|
-
<Input
|
|
65
|
-
value={name}
|
|
66
|
-
onChange={(event) => setName(event.target.value)}
|
|
67
|
-
placeholder="What should the API greet?"
|
|
68
|
-
/>
|
|
69
|
-
|
|
70
|
-
<Button type="primary" loading={loading} onClick={callApi}>
|
|
71
|
-
Call /pages/home/api/hello
|
|
72
|
-
</Button>
|
|
73
|
-
|
|
74
|
-
<Typography.Paragraph style={{ marginBottom: 0 }}>{messageText}</Typography.Paragraph>
|
|
75
|
-
</Space>
|
|
76
|
-
</Card>
|
|
77
|
-
</div>
|
|
78
|
-
)
|
|
79
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { testApi } from '@faasjs/dev'
|
|
2
|
-
import { describe, it, expect } from 'vitest'
|
|
3
|
-
|
|
4
|
-
import api from '../hello.api'
|
|
5
|
-
|
|
6
|
-
describe('pages/home/api/hello', () => {
|
|
7
|
-
it('should work', async () => {
|
|
8
|
-
const handler = testApi(api)
|
|
9
|
-
|
|
10
|
-
const { statusCode, data } = await handler({ name: 'world' })
|
|
11
|
-
|
|
12
|
-
expect(statusCode).toEqual(200)
|
|
13
|
-
expect(data).toEqual({
|
|
14
|
-
message: 'Hello, world!',
|
|
15
|
-
})
|
|
16
|
-
})
|
|
17
|
-
})
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|