create-qpq-app 0.1.6 → 0.1.7
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/lib/commonjs/steps/013_printNextSteps.js +1 -1
- package/lib/commonjs/steps/013_printNextSteps.js.map +1 -1
- package/lib/esm/steps/013_printNextSteps.js +1 -1
- package/lib/esm/steps/013_printNextSteps.js.map +1 -1
- package/package.json +2 -2
- package/template/README.md +5 -5
- package/template/apps/qpqjs/services/design/views/src/components/LandingPage/components/Footer.tsx +3 -2
- package/template/apps/qpqjs/services/design/views/src/components/LandingPage/components/InstallChip.tsx +1 -1
- package/template/docusaurus/docs/actions/core/ai/ask-ai-prompt-stream.md +1 -1
- package/template/docusaurus/docs/actions/core/ai/ask-ai-prompt.md +1 -0
- package/template/docusaurus/docs/getting-started.md +93 -754
- package/template/docusaurus/docs/index.md +7 -2
- package/template/package.json +2 -1
|
@@ -23,7 +23,7 @@ deploys as a single docker image.
|
|
|
23
23
|
Next steps:
|
|
24
24
|
|
|
25
25
|
cd ${answers.appName}
|
|
26
|
-
${install} npm run go:dev #
|
|
26
|
+
${install} npm run go:dev # api on http://localhost:8080, web on http://localhost:3080
|
|
27
27
|
npm run go # build the docker image (then run the printed docker command)
|
|
28
28
|
|
|
29
29
|
Deploy config lives in apps/${answers.appName}/deploy.config.json (domain: ${answers.domain}).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"013_printNextSteps.js","sourceRoot":"","sources":["../../../src/steps/013_printNextSteps.ts"],"names":[],"mappings":";;;;;;;;;;;;AAEa,QAAA,cAAc,GAAqB;IAC9C,IAAI,EAAE,MAAM;IAEZ,GAAG,EAAE,KAAoB,EAAE,4CAAf,EAAE,OAAO,EAAE;QACrB,MAAM,OAAO,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,kCAAkC,CAAC;QAEtF,OAAO,CAAC,GAAG,CAAC;UACN,OAAO,CAAC,OAAO;;;;;;;OAOlB,OAAO,CAAC,OAAO;EACpB,OAAO;;;8BAGqB,OAAO,CAAC,OAAO,gCAAgC,OAAO,CAAC,MAAM;CAC1F,CAAC,CAAC;IACD,CAAC,CAAA;CACF,CAAC","sourcesContent":["import { CreateQpqAppStep } from '../types';\n\nexport const printNextSteps: CreateQpqAppStep = {\n name: 'Done',\n\n run: async ({ answers }) => {\n const install = answers.installDependencies ? '' : ' npm install\\n npm run build\\n';\n\n console.log(`\nCreated ${answers.appName}!\n\nYour app has five services — admin, auth, design, shell and todo — and\ndeploys as a single docker image.\n\nNext steps:\n\n cd ${answers.appName}\n${install} npm run go:dev #
|
|
1
|
+
{"version":3,"file":"013_printNextSteps.js","sourceRoot":"","sources":["../../../src/steps/013_printNextSteps.ts"],"names":[],"mappings":";;;;;;;;;;;;AAEa,QAAA,cAAc,GAAqB;IAC9C,IAAI,EAAE,MAAM;IAEZ,GAAG,EAAE,KAAoB,EAAE,4CAAf,EAAE,OAAO,EAAE;QACrB,MAAM,OAAO,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,kCAAkC,CAAC;QAEtF,OAAO,CAAC,GAAG,CAAC;UACN,OAAO,CAAC,OAAO;;;;;;;OAOlB,OAAO,CAAC,OAAO;EACpB,OAAO;;;8BAGqB,OAAO,CAAC,OAAO,gCAAgC,OAAO,CAAC,MAAM;CAC1F,CAAC,CAAC;IACD,CAAC,CAAA;CACF,CAAC","sourcesContent":["import { CreateQpqAppStep } from '../types';\n\nexport const printNextSteps: CreateQpqAppStep = {\n name: 'Done',\n\n run: async ({ answers }) => {\n const install = answers.installDependencies ? '' : ' npm install\\n npm run build\\n';\n\n console.log(`\nCreated ${answers.appName}!\n\nYour app has five services — admin, auth, design, shell and todo — and\ndeploys as a single docker image.\n\nNext steps:\n\n cd ${answers.appName}\n${install} npm run go:dev # api on http://localhost:8080, web on http://localhost:3080\n npm run go # build the docker image (then run the printed docker command)\n\nDeploy config lives in apps/${answers.appName}/deploy.config.json (domain: ${answers.domain}).\n`);\n },\n};\n"]}
|
|
@@ -11,7 +11,7 @@ deploys as a single docker image.
|
|
|
11
11
|
Next steps:
|
|
12
12
|
|
|
13
13
|
cd ${answers.appName}
|
|
14
|
-
${install} npm run go:dev #
|
|
14
|
+
${install} npm run go:dev # api on http://localhost:8080, web on http://localhost:3080
|
|
15
15
|
npm run go # build the docker image (then run the printed docker command)
|
|
16
16
|
|
|
17
17
|
Deploy config lives in apps/${answers.appName}/deploy.config.json (domain: ${answers.domain}).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"013_printNextSteps.js","sourceRoot":"","sources":["../../../src/steps/013_printNextSteps.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,cAAc,GAAqB;IAC9C,IAAI,EAAE,MAAM;IAEZ,GAAG,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE;QACzB,MAAM,OAAO,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,kCAAkC,CAAC;QAEtF,OAAO,CAAC,GAAG,CAAC;UACN,OAAO,CAAC,OAAO;;;;;;;OAOlB,OAAO,CAAC,OAAO;EACpB,OAAO;;;8BAGqB,OAAO,CAAC,OAAO,gCAAgC,OAAO,CAAC,MAAM;CAC1F,CAAC,CAAC;IACD,CAAC;CACF,CAAC","sourcesContent":["import { CreateQpqAppStep } from '../types';\n\nexport const printNextSteps: CreateQpqAppStep = {\n name: 'Done',\n\n run: async ({ answers }) => {\n const install = answers.installDependencies ? '' : ' npm install\\n npm run build\\n';\n\n console.log(`\nCreated ${answers.appName}!\n\nYour app has five services — admin, auth, design, shell and todo — and\ndeploys as a single docker image.\n\nNext steps:\n\n cd ${answers.appName}\n${install} npm run go:dev #
|
|
1
|
+
{"version":3,"file":"013_printNextSteps.js","sourceRoot":"","sources":["../../../src/steps/013_printNextSteps.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,cAAc,GAAqB;IAC9C,IAAI,EAAE,MAAM;IAEZ,GAAG,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE;QACzB,MAAM,OAAO,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,kCAAkC,CAAC;QAEtF,OAAO,CAAC,GAAG,CAAC;UACN,OAAO,CAAC,OAAO;;;;;;;OAOlB,OAAO,CAAC,OAAO;EACpB,OAAO;;;8BAGqB,OAAO,CAAC,OAAO,gCAAgC,OAAO,CAAC,MAAM;CAC1F,CAAC,CAAC;IACD,CAAC;CACF,CAAC","sourcesContent":["import { CreateQpqAppStep } from '../types';\n\nexport const printNextSteps: CreateQpqAppStep = {\n name: 'Done',\n\n run: async ({ answers }) => {\n const install = answers.installDependencies ? '' : ' npm install\\n npm run build\\n';\n\n console.log(`\nCreated ${answers.appName}!\n\nYour app has five services — admin, auth, design, shell and todo — and\ndeploys as a single docker image.\n\nNext steps:\n\n cd ${answers.appName}\n${install} npm run go:dev # api on http://localhost:8080, web on http://localhost:3080\n npm run go # build the docker image (then run the printed docker command)\n\nDeploy config lives in apps/${answers.appName}/deploy.config.json (domain: ${answers.domain}).\n`);\n },\n};\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-qpq-app",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Scaffold a new quidproquo app — npx create-qpq-app my-app",
|
|
5
5
|
"main": "./lib/commonjs/index.js",
|
|
6
6
|
"module": "./lib/esm/index.js",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/node": "^22.13.13",
|
|
54
|
-
"quidproquo-tsconfig": "0.1.
|
|
54
|
+
"quidproquo-tsconfig": "0.1.7"
|
|
55
55
|
},
|
|
56
56
|
"bin": {
|
|
57
57
|
"create-qpq-app": "./lib/commonjs/bin/createQpqApp.js"
|
package/template/README.md
CHANGED
|
@@ -21,8 +21,7 @@ always runs against the in-repo HEAD. Build the framework first
|
|
|
21
21
|
nvm use # ALWAYS FIRST — switches to the Node version in .nvmrc
|
|
22
22
|
npm install # installs deps + symlinks the sibling quidproquo-* packages (file: refs)
|
|
23
23
|
npm run build # build every workspace (libs -> tsc -b to dist; services -> type-check)
|
|
24
|
-
npm run go:dev #
|
|
25
|
-
npm run go:dev:web # views dev servers (host + one Rspack server per remote)
|
|
24
|
+
npm run go:dev # full local dev stack: api dev server (:8080) + all views dev servers
|
|
26
25
|
```
|
|
27
26
|
|
|
28
27
|
Smoke test once `go:dev` is up:
|
|
@@ -74,7 +73,7 @@ generic CDK app in `quidproquo-deploy-awscdk`.
|
|
|
74
73
|
`defineDevServerOptions({ port })` in its `infrastructure.ts`). The
|
|
75
74
|
only generated artifact is `apps/<app>/tsconfig.federated.json` — regenerate
|
|
76
75
|
with `npm run prep` after adding or removing a marker.
|
|
77
|
-
- **The dev server** (`qpq go:dev`, powered by `quidproquo-dev-server`) loads each service's
|
|
76
|
+
- **The dev server** (`qpq go:dev:api`, powered by `quidproquo-dev-server`) loads each service's
|
|
78
77
|
`infrastructure.ts` directly — no synth needed, just built libs. Its bundle
|
|
79
78
|
externalizes `node_modules` (an externals function in its rspack config) so native modules like
|
|
80
79
|
`sqlite3` load at runtime from their install location.
|
|
@@ -90,7 +89,8 @@ generic CDK app in `quidproquo-deploy-awscdk`.
|
|
|
90
89
|
npm run build # build only the libs (services/views are bundled, not built); scope with --workspace=@qpqjs/constants
|
|
91
90
|
npm run validate-ts # TS check: build libs, then tsc-typecheck every service + views
|
|
92
91
|
npm run prep # regenerate tsconfig.federated.json from // federated.export markers
|
|
93
|
-
npm run go:dev #
|
|
92
|
+
npm run go:dev # api + web dev servers in one process (one ctrl+c stops the lot)
|
|
93
|
+
npm run go:dev:api # backend dev server only, on :8080, every service at /api/<svc>
|
|
94
94
|
npm run go:dev:web # boot ALL views Rspack dev servers (host shell :3080 + remotes on their
|
|
95
95
|
# ports); remotes resolve dynamically at runtime via mf-manifest.json.
|
|
96
96
|
# Subset: npm run go:dev:web -- --only host,shell,design
|
|
@@ -99,7 +99,7 @@ npm run go:dev:web # boot ALL views Rspack dev servers (host shell :3080 + rem
|
|
|
99
99
|
### App selection (multi-app)
|
|
100
100
|
|
|
101
101
|
Every app-facing script (`prep`, `synth`, `go`, `go:docker`, `go:dev`,
|
|
102
|
-
`go:dev:web`) resolves its target app the same way:
|
|
102
|
+
`go:dev:api`, `go:dev:web`) resolves its target app the same way:
|
|
103
103
|
|
|
104
104
|
1. `--app <name>` — **must come after `--`**: `npm run go:dev -- --app qpqjs`
|
|
105
105
|
(this npm silently swallows flags before `--`)
|
package/template/apps/qpqjs/services/design/views/src/components/LandingPage/components/Footer.tsx
CHANGED
|
@@ -18,8 +18,9 @@ export function Footer() {
|
|
|
18
18
|
<span className="hero__title-glow">your first story?</span>
|
|
19
19
|
</h2>
|
|
20
20
|
<p className="footer__cta-sub">
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
One command scaffolds a full app — five services, a local dev
|
|
22
|
+
server, a one-image docker deploy. Yield an action; the runtime
|
|
23
|
+
handles the rest.
|
|
23
24
|
</p>
|
|
24
25
|
<div className="footer__cta-actions">
|
|
25
26
|
<InstallChip />
|
|
@@ -42,7 +42,7 @@ The parameters are identical to [askAiPrompt](./ask-ai-prompt.md) — see there
|
|
|
42
42
|
| --- | --- | --- |
|
|
43
43
|
| `model` | `AiModel` | Which model to prompt. |
|
|
44
44
|
| `prompt` | `string` | The user prompt. Ignored when `options.messages` is set. |
|
|
45
|
-
| `options` | `AskAiPromptStreamOptions` | `{ system?, aiName?, messages?, reasoning? }` — same shape and meaning as [`AskAiPromptOptions`](./ask-ai-prompt.md#askaipromptoptions). |
|
|
45
|
+
| `options` | `AskAiPromptStreamOptions` | `{ system?, aiName?, messages?, reasoning?, caching? }` — same shape and meaning as [`AskAiPromptOptions`](./ask-ai-prompt.md#askaipromptoptions). |
|
|
46
46
|
|
|
47
47
|
## Returns
|
|
48
48
|
|
|
@@ -49,6 +49,7 @@ function* askAiPrompt(
|
|
|
49
49
|
| `aiName` | `string` | – | Name of a [defineAi](../../../config/core/ai.md) config to bind. This is what wires up tool definitions (and their executors) for the model to call. Omit for a plain, tool-less prompt. |
|
|
50
50
|
| `messages` | [`AiMessage[]`](#aimessage) | – | A full conversation history. When present, this is sent instead of `prompt`, letting you carry a multi-turn dialogue (including prior assistant turns and tool results). |
|
|
51
51
|
| `reasoning` | [`AiReasoningConfig`](#aireasoningconfig) | – | Enables extended thinking. Its presence turns reasoning on; `budgetTokens` caps how many tokens the model may spend thinking before it answers (defaults to `4096` on AWS). |
|
|
52
|
+
| `caching` | `boolean` | – | Marks the system prompt and the last message (or the last `messages` entry) with a Bedrock cache point, so a following call in the same conversation can read everything up to there from cache instead of reprocessing it. |
|
|
52
53
|
|
|
53
54
|
### `AiModel`
|
|
54
55
|
|
|
@@ -4,803 +4,142 @@ sidebar_position: 1
|
|
|
4
4
|
|
|
5
5
|
# Getting Started
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
## Prerequisites
|
|
10
|
-
|
|
11
|
-
Before you begin, ensure you have:
|
|
12
|
-
- Node.js 18+ installed
|
|
13
|
-
- npm or yarn package manager
|
|
14
|
-
- Basic knowledge of JavaScript/TypeScript
|
|
15
|
-
- Familiarity with generator functions (we'll explain the basics)
|
|
16
|
-
|
|
17
|
-
## Installation
|
|
18
|
-
|
|
19
|
-
### Create a New Project
|
|
7
|
+
The fastest way to a running quidproquo app is one command:
|
|
20
8
|
|
|
21
9
|
```bash
|
|
22
|
-
|
|
23
|
-
mkdir my-qpq-app
|
|
24
|
-
cd my-qpq-app
|
|
25
|
-
|
|
26
|
-
# Initialize package.json
|
|
27
|
-
npm init -y
|
|
28
|
-
|
|
29
|
-
# Install TypeScript (recommended)
|
|
30
|
-
npm install --save-dev typescript @types/node
|
|
10
|
+
npx create-qpq-app myapp
|
|
31
11
|
```
|
|
32
12
|
|
|
33
|
-
|
|
13
|
+
That scaffolds a complete, working application — five services, a local dev
|
|
14
|
+
server, and a one-image docker deploy — then installs its dependencies,
|
|
15
|
+
builds it, and makes the first git commit for you.
|
|
34
16
|
|
|
35
17
|
```bash
|
|
36
|
-
|
|
37
|
-
npm
|
|
38
|
-
|
|
39
|
-
# Web server package (for APIs)
|
|
40
|
-
npm install quidproquo-webserver
|
|
41
|
-
|
|
42
|
-
# Development server (for local development)
|
|
43
|
-
npm install --save-dev quidproquo-dev-server
|
|
44
|
-
|
|
45
|
-
# Platform-specific processors (choose based on deployment target)
|
|
46
|
-
npm install quidproquo-actionprocessor-node # For Node.js
|
|
47
|
-
# OR
|
|
48
|
-
npm install quidproquo-actionprocessor-awslambda # For AWS
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
### TypeScript Configuration
|
|
52
|
-
|
|
53
|
-
Create a `tsconfig.json`:
|
|
54
|
-
|
|
55
|
-
```json
|
|
56
|
-
{
|
|
57
|
-
"compilerOptions": {
|
|
58
|
-
"target": "ES2020",
|
|
59
|
-
"module": "commonjs",
|
|
60
|
-
"lib": ["ES2020"],
|
|
61
|
-
"outDir": "./dist",
|
|
62
|
-
"rootDir": "./src",
|
|
63
|
-
"strict": true,
|
|
64
|
-
"esModuleInterop": true,
|
|
65
|
-
"skipLibCheck": true,
|
|
66
|
-
"forceConsistentCasingInFileNames": true,
|
|
67
|
-
"resolveJsonModule": true,
|
|
68
|
-
"declaration": true,
|
|
69
|
-
"declarationMap": true
|
|
70
|
-
},
|
|
71
|
-
"include": ["src/**/*"],
|
|
72
|
-
"exclude": ["node_modules", "dist"]
|
|
73
|
-
}
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
## Your First Story
|
|
77
|
-
|
|
78
|
-
Let's create a simple "Hello World" story to understand the basics.
|
|
79
|
-
|
|
80
|
-
### Understanding Generator Functions
|
|
81
|
-
|
|
82
|
-
Generator functions (marked with `*`) can pause and resume execution:
|
|
83
|
-
|
|
84
|
-
```typescript
|
|
85
|
-
// src/stories/hello.ts
|
|
86
|
-
import { askDateNow, askLogCreate } from 'quidproquo-core';
|
|
87
|
-
|
|
88
|
-
export function* helloWorldStory(name: string) {
|
|
89
|
-
// Log the start
|
|
90
|
-
yield* askLogCreate('INFO', `Starting hello for ${name}`);
|
|
91
|
-
|
|
92
|
-
// Get current time
|
|
93
|
-
const currentTime = yield* askDateNow();
|
|
94
|
-
|
|
95
|
-
// Return greeting
|
|
96
|
-
return `Hello ${name}! The time is ${currentTime}`;
|
|
97
|
-
}
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
**Key concepts:**
|
|
101
|
-
- `function*` declares a generator function
|
|
102
|
-
- `yield*` pauses execution and delegates to another generator
|
|
103
|
-
- Each `ask*` function returns a generator that yields an action
|
|
104
|
-
- The runtime processes each action and returns the result
|
|
105
|
-
|
|
106
|
-
### Creating Actions
|
|
107
|
-
|
|
108
|
-
Actions describe what you want to do:
|
|
109
|
-
|
|
110
|
-
```typescript
|
|
111
|
-
// src/stories/user.ts
|
|
112
|
-
import {
|
|
113
|
-
askKeyValueStoreGet,
|
|
114
|
-
askKeyValueStoreUpsert,
|
|
115
|
-
askGuidNew,
|
|
116
|
-
askDateNow,
|
|
117
|
-
askThrowError
|
|
118
|
-
} from 'quidproquo-core';
|
|
119
|
-
|
|
120
|
-
export function* createUserStory(email: string, name: string) {
|
|
121
|
-
// Check if user exists
|
|
122
|
-
const existingUser = yield* askKeyValueStoreGet('users', email);
|
|
123
|
-
|
|
124
|
-
if (existingUser) {
|
|
125
|
-
// Throw an error if user exists
|
|
126
|
-
yield* askThrowError('USER_EXISTS', `User with email ${email} already exists`);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Generate a new ID
|
|
130
|
-
const userId = yield* askGuidNew();
|
|
131
|
-
|
|
132
|
-
// Get current timestamp
|
|
133
|
-
const createdAt = yield* askDateNow();
|
|
134
|
-
|
|
135
|
-
// Create user object
|
|
136
|
-
const user = {
|
|
137
|
-
id: userId,
|
|
138
|
-
email,
|
|
139
|
-
name,
|
|
140
|
-
createdAt
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
// Store the user
|
|
144
|
-
yield* askKeyValueStoreUpsert('users', user);
|
|
145
|
-
|
|
146
|
-
return user;
|
|
147
|
-
}
|
|
18
|
+
cd myapp
|
|
19
|
+
npm run go:dev # api on http://localhost:8080, web on http://localhost:3080
|
|
148
20
|
```
|
|
149
21
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
### Define Routes
|
|
22
|
+
One command, one ctrl+c: `go:dev` runs the backend dev server and every views
|
|
23
|
+
dev server in a single process. Want just one side? `npm run go:dev:api` and
|
|
24
|
+
`npm run go:dev:web` run them separately.
|
|
155
25
|
|
|
156
|
-
|
|
157
|
-
// src/api/index.ts
|
|
158
|
-
import { HTTPMethod, HTTPResponse } from 'quidproquo-webserver';
|
|
159
|
-
import { createUserStory } from '../stories/user';
|
|
160
|
-
import { askCatch } from 'quidproquo-core';
|
|
26
|
+
Check it's alive:
|
|
161
27
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
// Validate input
|
|
166
|
-
if (!email || !name) {
|
|
167
|
-
return {
|
|
168
|
-
statusCode: 400,
|
|
169
|
-
headers: { 'Content-Type': 'application/json' },
|
|
170
|
-
body: JSON.stringify({ error: 'Email and name are required' })
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Try to create user
|
|
175
|
-
const result = yield* askCatch(createUserStory(email, name));
|
|
176
|
-
|
|
177
|
-
if (result.success) {
|
|
178
|
-
return {
|
|
179
|
-
statusCode: 201,
|
|
180
|
-
headers: { 'Content-Type': 'application/json' },
|
|
181
|
-
body: JSON.stringify(result.result)
|
|
182
|
-
};
|
|
183
|
-
} else {
|
|
184
|
-
return {
|
|
185
|
-
statusCode: 400,
|
|
186
|
-
headers: { 'Content-Type': 'application/json' },
|
|
187
|
-
body: JSON.stringify({ error: result.error.errorText })
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
### Configure the API
|
|
194
|
-
|
|
195
|
-
```typescript
|
|
196
|
-
// src/config.ts
|
|
197
|
-
import { defineRoute, defineApi, defineService } from 'quidproquo-webserver';
|
|
198
|
-
import { defineKeyValueStore } from 'quidproquo-core';
|
|
199
|
-
import { handleCreateUser } from './api';
|
|
200
|
-
|
|
201
|
-
export default [
|
|
202
|
-
// Define a key-value store for users
|
|
203
|
-
defineKeyValueStore('users', {
|
|
204
|
-
partitionKey: 'email',
|
|
205
|
-
indexes: []
|
|
206
|
-
}),
|
|
207
|
-
|
|
208
|
-
// Define API routes
|
|
209
|
-
defineApi('main', {
|
|
210
|
-
routes: [
|
|
211
|
-
defineRoute(HTTPMethod.POST, '/users', handleCreateUser),
|
|
212
|
-
defineRoute(HTTPMethod.GET, '/health', function* () {
|
|
213
|
-
return {
|
|
214
|
-
statusCode: 200,
|
|
215
|
-
body: JSON.stringify({ status: 'healthy' })
|
|
216
|
-
};
|
|
217
|
-
})
|
|
218
|
-
]
|
|
219
|
-
}),
|
|
220
|
-
|
|
221
|
-
// Define the service
|
|
222
|
-
defineService('my-app', {
|
|
223
|
-
apis: ['main']
|
|
224
|
-
})
|
|
225
|
-
];
|
|
28
|
+
```bash
|
|
29
|
+
curl http://localhost:8080/api/shell/v1/health
|
|
30
|
+
# {"status":"healthy","service":"shell","checkedAt":"..."}
|
|
226
31
|
```
|
|
227
32
|
|
|
228
|
-
##
|
|
229
|
-
|
|
230
|
-
The development server simulates cloud services locally.
|
|
33
|
+
## Prerequisites
|
|
231
34
|
|
|
232
|
-
|
|
35
|
+
- **Node.js 24 or newer** — the scaffolder checks and refuses politely on
|
|
36
|
+
older versions
|
|
37
|
+
- **Docker** — only needed when you deploy (`npm run go`); local dev doesn't
|
|
38
|
+
use it
|
|
233
39
|
|
|
234
|
-
|
|
235
|
-
// src/dev.ts
|
|
236
|
-
import { createDevServer } from 'quidproquo-dev-server';
|
|
237
|
-
import config from './config';
|
|
40
|
+
## What you get
|
|
238
41
|
|
|
239
|
-
|
|
240
|
-
config,
|
|
241
|
-
port: 3000,
|
|
242
|
-
watch: true
|
|
243
|
-
});
|
|
42
|
+
`create-qpq-app` generates an npm-workspaces monorepo:
|
|
244
43
|
|
|
245
|
-
server.start().then(() => {
|
|
246
|
-
console.log('Dev server running on http://localhost:3000');
|
|
247
|
-
});
|
|
248
44
|
```
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
45
|
+
myapp/
|
|
46
|
+
├── package.json # workspace root — run all commands from here
|
|
47
|
+
└── apps/myapp/
|
|
48
|
+
├── deploy.config.json # environments: development + production (docker)
|
|
49
|
+
├── packages/
|
|
50
|
+
│ ├── constants/ # domain, service enum, user directory name
|
|
51
|
+
│ ├── config/
|
|
52
|
+
│ └── service-utils/ # the shared config every service starts from
|
|
53
|
+
└── services/
|
|
54
|
+
├── shell/ # module-federation host — the root website
|
|
55
|
+
├── design/ # shared UI — federated views other services import
|
|
56
|
+
├── auth/ # authentication service
|
|
57
|
+
├── admin/ # the qpq admin console
|
|
58
|
+
└── todo/ # the example feature: an api service + TodoList view
|
|
260
59
|
```
|
|
261
60
|
|
|
262
|
-
|
|
61
|
+
Every service follows the same layout: a `service/` package holding its
|
|
62
|
+
infrastructure config and controllers, an optional `views/` microfrontend,
|
|
63
|
+
and small lib packages (`models`, `shared-logic`, `service-utils`, …) for
|
|
64
|
+
code you'll share between them.
|
|
263
65
|
|
|
264
|
-
|
|
265
|
-
npm run dev
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
Your API is now running! Test it:
|
|
66
|
+
## Your first story
|
|
269
67
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
-H "Content-Type: application/json" \
|
|
274
|
-
-d '{"email": "alice@example.com", "name": "Alice"}'
|
|
68
|
+
Business logic in quidproquo is a **story** — a generator function that
|
|
69
|
+
yields typed actions. The scaffolded shell service ships one: the health
|
|
70
|
+
route you just curled.
|
|
275
71
|
|
|
276
|
-
|
|
277
|
-
curl http://localhost:3000/health
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
## Complete Example: Todo API
|
|
281
|
-
|
|
282
|
-
Let's build a more complete example - a Todo API with CRUD operations.
|
|
283
|
-
|
|
284
|
-
### Define Todo Stories
|
|
285
|
-
|
|
286
|
-
```typescript
|
|
287
|
-
// src/stories/todo.ts
|
|
72
|
+
```typescript title="apps/myapp/services/shell/service/src/entry/controller/health/health.ts"
|
|
288
73
|
import {
|
|
289
|
-
askKeyValueStoreGet,
|
|
290
|
-
askKeyValueStoreUpsert,
|
|
291
|
-
askKeyValueStoreDelete,
|
|
292
|
-
askKeyValueStoreQuery,
|
|
293
|
-
askGuidNew,
|
|
294
74
|
askDateNow,
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
id: todoId,
|
|
313
|
-
userId,
|
|
314
|
-
title,
|
|
315
|
-
completed: false,
|
|
316
|
-
createdAt: now,
|
|
317
|
-
updatedAt: now
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
yield* askKeyValueStoreUpsert('todos', todo);
|
|
321
|
-
return todo;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
export function* getTodoStory(todoId: string, userId: string) {
|
|
325
|
-
const todo = yield* askKeyValueStoreGet('todos', todoId);
|
|
326
|
-
|
|
327
|
-
if (!todo) {
|
|
328
|
-
yield* askThrowError('NOT_FOUND', 'Todo not found');
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
if (todo.userId !== userId) {
|
|
332
|
-
yield* askThrowError('FORBIDDEN', 'Access denied');
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
return todo;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
export function* updateTodoStory(
|
|
339
|
-
todoId: string,
|
|
340
|
-
userId: string,
|
|
341
|
-
updates: Partial<Pick<Todo, 'title' | 'completed'>>
|
|
342
|
-
) {
|
|
343
|
-
// Get existing todo
|
|
344
|
-
const todo = yield* getTodoStory(todoId, userId);
|
|
345
|
-
|
|
346
|
-
// Update fields
|
|
347
|
-
const updatedTodo = {
|
|
348
|
-
...todo,
|
|
349
|
-
...updates,
|
|
350
|
-
updatedAt: yield* askDateNow()
|
|
351
|
-
};
|
|
352
|
-
|
|
353
|
-
yield* askKeyValueStoreUpsert('todos', updatedTodo);
|
|
354
|
-
return updatedTodo;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
export function* deleteTodoStory(todoId: string, userId: string) {
|
|
358
|
-
// Verify ownership
|
|
359
|
-
yield* getTodoStory(todoId, userId);
|
|
360
|
-
|
|
361
|
-
// Delete
|
|
362
|
-
yield* askKeyValueStoreDelete('todos', todoId);
|
|
363
|
-
return { success: true };
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
export function* listTodosStory(userId: string) {
|
|
367
|
-
const todos = yield* askKeyValueStoreQuery('todos', {
|
|
368
|
-
index: 'byUser',
|
|
369
|
-
keyCondition: {
|
|
370
|
-
userId: { '=': userId }
|
|
371
|
-
}
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
return todos;
|
|
375
|
-
}
|
|
376
|
-
```
|
|
377
|
-
|
|
378
|
-
### Create API Handlers
|
|
379
|
-
|
|
380
|
-
```typescript
|
|
381
|
-
// src/api/todos.ts
|
|
382
|
-
import { HTTPRequest, HTTPResponse } from 'quidproquo-webserver';
|
|
383
|
-
import { askCatch } from 'quidproquo-core';
|
|
384
|
-
import * as todoStories from '../stories/todo';
|
|
385
|
-
|
|
386
|
-
export function* createTodo(request: HTTPRequest): Generator<any, HTTPResponse, any> {
|
|
387
|
-
const { title } = JSON.parse(request.body);
|
|
388
|
-
const userId = request.headers['x-user-id']; // Simplified auth
|
|
389
|
-
|
|
390
|
-
if (!title) {
|
|
391
|
-
return { statusCode: 400, body: JSON.stringify({ error: 'Title required' }) };
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
const todo = yield* todoStories.createTodoStory(userId, title);
|
|
395
|
-
return { statusCode: 201, body: JSON.stringify(todo) };
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
export function* getTodo(request: HTTPRequest): Generator<any, HTTPResponse, any> {
|
|
399
|
-
const todoId = request.pathParameters.id;
|
|
400
|
-
const userId = request.headers['x-user-id'];
|
|
401
|
-
|
|
402
|
-
const result = yield* askCatch(todoStories.getTodoStory(todoId, userId));
|
|
403
|
-
|
|
404
|
-
if (!result.success) {
|
|
405
|
-
const statusCode = result.error.errorType === 'NOT_FOUND' ? 404 : 403;
|
|
406
|
-
return { statusCode, body: JSON.stringify({ error: result.error.errorText }) };
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
return { statusCode: 200, body: JSON.stringify(result.result) };
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
export function* updateTodo(request: HTTPRequest): Generator<any, HTTPResponse, any> {
|
|
413
|
-
const todoId = request.pathParameters.id;
|
|
414
|
-
const userId = request.headers['x-user-id'];
|
|
415
|
-
const updates = JSON.parse(request.body);
|
|
416
|
-
|
|
417
|
-
const result = yield* askCatch(
|
|
418
|
-
todoStories.updateTodoStory(todoId, userId, updates)
|
|
419
|
-
);
|
|
420
|
-
|
|
421
|
-
if (!result.success) {
|
|
422
|
-
return { statusCode: 400, body: JSON.stringify({ error: result.error.errorText }) };
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
return { statusCode: 200, body: JSON.stringify(result.result) };
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
export function* deleteTodo(request: HTTPRequest): Generator<any, HTTPResponse, any> {
|
|
429
|
-
const todoId = request.pathParameters.id;
|
|
430
|
-
const userId = request.headers['x-user-id'];
|
|
431
|
-
|
|
432
|
-
const result = yield* askCatch(todoStories.deleteTodoStory(todoId, userId));
|
|
433
|
-
|
|
434
|
-
if (!result.success) {
|
|
435
|
-
return { statusCode: 400, body: JSON.stringify({ error: result.error.errorText }) };
|
|
75
|
+
AskResponse,
|
|
76
|
+
HTTPEvent,
|
|
77
|
+
HTTPEventResponse,
|
|
78
|
+
qpqWebServerUtils,
|
|
79
|
+
} from 'quidproquo';
|
|
80
|
+
import { dynamicRoute } from 'quidproquo-features';
|
|
81
|
+
|
|
82
|
+
export const health = dynamicRoute(
|
|
83
|
+
['GET', '/health'],
|
|
84
|
+
function* healthCheck(event: HTTPEvent): AskResponse<HTTPEventResponse> {
|
|
85
|
+
const checkedAt = yield* askDateNow();
|
|
86
|
+
|
|
87
|
+
return qpqWebServerUtils.toJsonEventResponse({
|
|
88
|
+
status: 'healthy',
|
|
89
|
+
service: 'shell',
|
|
90
|
+
checkedAt,
|
|
91
|
+
});
|
|
436
92
|
}
|
|
437
|
-
|
|
438
|
-
return { statusCode: 204, body: '' };
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
export function* listTodos(request: HTTPRequest): Generator<any, HTTPResponse, any> {
|
|
442
|
-
const userId = request.headers['x-user-id'];
|
|
443
|
-
const todos = yield* todoStories.listTodosStory(userId);
|
|
444
|
-
return { statusCode: 200, body: JSON.stringify(todos) };
|
|
445
|
-
}
|
|
93
|
+
);
|
|
446
94
|
```
|
|
447
95
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
import { defineRoute, defineApi, HTTPMethod } from 'quidproquo-webserver';
|
|
453
|
-
import { defineKeyValueStore } from 'quidproquo-core';
|
|
454
|
-
import * as todoHandlers from '../api/todos';
|
|
455
|
-
|
|
456
|
-
export default [
|
|
457
|
-
// Define storage
|
|
458
|
-
defineKeyValueStore('todos', {
|
|
459
|
-
partitionKey: 'id',
|
|
460
|
-
indexes: [
|
|
461
|
-
{
|
|
462
|
-
name: 'byUser',
|
|
463
|
-
partitionKey: 'userId',
|
|
464
|
-
sortKey: 'createdAt'
|
|
465
|
-
}
|
|
466
|
-
]
|
|
467
|
-
}),
|
|
468
|
-
|
|
469
|
-
// Define API
|
|
470
|
-
defineApi('todos', {
|
|
471
|
-
routes: [
|
|
472
|
-
defineRoute(HTTPMethod.POST, '/todos', todoHandlers.createTodo),
|
|
473
|
-
defineRoute(HTTPMethod.GET, '/todos', todoHandlers.listTodos),
|
|
474
|
-
defineRoute(HTTPMethod.GET, '/todos/:id', todoHandlers.getTodo),
|
|
475
|
-
defineRoute(HTTPMethod.PUT, '/todos/:id', todoHandlers.updateTodo),
|
|
476
|
-
defineRoute(HTTPMethod.DELETE, '/todos/:id', todoHandlers.deleteTodo),
|
|
477
|
-
]
|
|
478
|
-
})
|
|
479
|
-
];
|
|
480
|
-
```
|
|
96
|
+
The story never reads the clock itself — it *asks* for the time by yielding
|
|
97
|
+
an action, and the runtime answers. On your machine that's the dev server;
|
|
98
|
+
in production it's whatever platform the app is deployed to. Same story,
|
|
99
|
+
zero changes.
|
|
481
100
|
|
|
482
|
-
|
|
101
|
+
Add your own route by dropping a file next to `health.ts`, exporting it from
|
|
102
|
+
the controller `index.ts`, and saving — the dev server rebuilds and restarts
|
|
103
|
+
itself.
|
|
483
104
|
|
|
484
|
-
|
|
105
|
+
## Deploy it
|
|
485
106
|
|
|
486
|
-
|
|
107
|
+
Both scaffolded environments deploy as a single docker image — the whole
|
|
108
|
+
app, one container:
|
|
487
109
|
|
|
488
110
|
```bash
|
|
489
|
-
|
|
490
|
-
npm install --save-dev aws-cdk quidproquo-deploy-awscdk
|
|
491
|
-
npm install quidproquo-actionprocessor-awslambda
|
|
492
|
-
|
|
493
|
-
# Create CDK app
|
|
494
|
-
mkdir cdk
|
|
495
|
-
cd cdk
|
|
496
|
-
cdk init app --language typescript
|
|
497
|
-
```
|
|
498
|
-
|
|
499
|
-
Create CDK stack:
|
|
500
|
-
|
|
501
|
-
```typescript
|
|
502
|
-
// cdk/lib/my-app-stack.ts
|
|
503
|
-
import * as cdk from 'aws-cdk-lib';
|
|
504
|
-
import { QPQApp } from 'quidproquo-deploy-awscdk';
|
|
505
|
-
import config from '../../src/config';
|
|
506
|
-
|
|
507
|
-
export class MyAppStack extends cdk.Stack {
|
|
508
|
-
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
|
|
509
|
-
super(scope, id, props);
|
|
510
|
-
|
|
511
|
-
new QPQApp(this, 'MyQPQApp', {
|
|
512
|
-
qpqConfig: config,
|
|
513
|
-
environment: 'production'
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
}
|
|
111
|
+
npm run go
|
|
517
112
|
```
|
|
518
113
|
|
|
519
|
-
|
|
114
|
+
Pick an environment when prompted (or pass `--env development`), and the
|
|
115
|
+
build prints the `docker run` command when the image is ready:
|
|
520
116
|
|
|
521
117
|
```bash
|
|
522
|
-
|
|
118
|
+
docker run --rm -p 80:8080 -p 8080:8080 -p 8888:8888 -p 3001:3001 \
|
|
119
|
+
-v qpq-myapp-data:/app/.qpq-runtime \
|
|
120
|
+
qpq-myapp:development
|
|
523
121
|
```
|
|
524
122
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
// src/server.ts
|
|
529
|
-
import express from 'express';
|
|
530
|
-
import { createNodeRuntime } from 'quidproquo-actionprocessor-node';
|
|
531
|
-
import config from './config';
|
|
532
|
-
|
|
533
|
-
const app = express();
|
|
534
|
-
const runtime = createNodeRuntime(config);
|
|
535
|
-
|
|
536
|
-
app.use(express.json());
|
|
537
|
-
app.all('*', async (req, res) => {
|
|
538
|
-
const result = await runtime.handleHTTPRequest({
|
|
539
|
-
method: req.method,
|
|
540
|
-
path: req.path,
|
|
541
|
-
headers: req.headers,
|
|
542
|
-
body: JSON.stringify(req.body),
|
|
543
|
-
query: req.query
|
|
544
|
-
});
|
|
545
|
-
|
|
546
|
-
res.status(result.statusCode).json(JSON.parse(result.body));
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
app.listen(3000, () => {
|
|
550
|
-
console.log('Server running on port 3000');
|
|
551
|
-
});
|
|
552
|
-
```
|
|
553
|
-
|
|
554
|
-
### Deploy to Docker
|
|
555
|
-
|
|
556
|
-
Create a Dockerfile:
|
|
557
|
-
|
|
558
|
-
```dockerfile
|
|
559
|
-
FROM node:18-alpine
|
|
560
|
-
|
|
561
|
-
WORKDIR /app
|
|
562
|
-
|
|
563
|
-
COPY package*.json ./
|
|
564
|
-
RUN npm ci --production
|
|
565
|
-
|
|
566
|
-
COPY dist ./dist
|
|
567
|
-
COPY config ./config
|
|
123
|
+
Then open [http://localhost](http://localhost). Environments live in
|
|
124
|
+
`apps/myapp/deploy.config.json` — an app can move platforms (docker → AWS)
|
|
125
|
+
with a config change, not a rewrite.
|
|
568
126
|
|
|
569
|
-
|
|
570
|
-
```
|
|
571
|
-
|
|
572
|
-
Build and run:
|
|
127
|
+
## Options
|
|
573
128
|
|
|
574
129
|
```bash
|
|
575
|
-
|
|
576
|
-
docker build -t my-qpq-app .
|
|
577
|
-
docker run -p 3000:3000 my-qpq-app
|
|
578
|
-
```
|
|
579
|
-
|
|
580
|
-
## Testing Your Application
|
|
581
|
-
|
|
582
|
-
Quidproquo's architecture makes testing straightforward.
|
|
583
|
-
|
|
584
|
-
### Unit Testing Stories
|
|
585
|
-
|
|
586
|
-
```typescript
|
|
587
|
-
// src/stories/todo.test.ts
|
|
588
|
-
import { createTodoStory } from './todo';
|
|
589
|
-
|
|
590
|
-
test('creates todo with correct fields', () => {
|
|
591
|
-
const story = createTodoStory('user-123', 'Buy milk');
|
|
592
|
-
|
|
593
|
-
// First yield: Generate ID
|
|
594
|
-
const { value: guidAction } = story.next();
|
|
595
|
-
expect(guidAction.type).toBe('Guid::New');
|
|
596
|
-
|
|
597
|
-
// Provide mock ID
|
|
598
|
-
const { value: dateAction } = story.next('todo-456');
|
|
599
|
-
expect(dateAction.type).toBe('Date::Now');
|
|
600
|
-
|
|
601
|
-
// Provide mock date
|
|
602
|
-
const { value: storeAction } = story.next('2024-01-01T00:00:00Z');
|
|
603
|
-
expect(storeAction.type).toBe('KeyValueStore::Upsert');
|
|
604
|
-
expect(storeAction.payload.item).toEqual({
|
|
605
|
-
id: 'todo-456',
|
|
606
|
-
userId: 'user-123',
|
|
607
|
-
title: 'Buy milk',
|
|
608
|
-
completed: false,
|
|
609
|
-
createdAt: '2024-01-01T00:00:00Z',
|
|
610
|
-
updatedAt: '2024-01-01T00:00:00Z'
|
|
611
|
-
});
|
|
612
|
-
|
|
613
|
-
// Complete story
|
|
614
|
-
const { value: result } = story.next();
|
|
615
|
-
expect(result.id).toBe('todo-456');
|
|
616
|
-
});
|
|
617
|
-
```
|
|
618
|
-
|
|
619
|
-
### Integration Testing
|
|
620
|
-
|
|
621
|
-
```typescript
|
|
622
|
-
// src/api/todos.test.ts
|
|
623
|
-
import { createTestRuntime } from 'quidproquo-testing';
|
|
624
|
-
import { createTodo } from './todos';
|
|
625
|
-
|
|
626
|
-
test('creates todo via API', async () => {
|
|
627
|
-
const runtime = createTestRuntime({
|
|
628
|
-
stores: {
|
|
629
|
-
todos: new Map()
|
|
630
|
-
}
|
|
631
|
-
});
|
|
632
|
-
|
|
633
|
-
const request = {
|
|
634
|
-
body: JSON.stringify({ title: 'Test todo' }),
|
|
635
|
-
headers: { 'x-user-id': 'user-123' }
|
|
636
|
-
};
|
|
637
|
-
|
|
638
|
-
const response = await runtime.execute(createTodo, request);
|
|
639
|
-
|
|
640
|
-
expect(response.statusCode).toBe(201);
|
|
641
|
-
const todo = JSON.parse(response.body);
|
|
642
|
-
expect(todo.title).toBe('Test todo');
|
|
643
|
-
expect(todo.userId).toBe('user-123');
|
|
644
|
-
});
|
|
645
|
-
```
|
|
130
|
+
npx create-qpq-app <app-name> [options]
|
|
646
131
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
### Enable Debug Logging
|
|
652
|
-
|
|
653
|
-
```typescript
|
|
654
|
-
// Set environment variable
|
|
655
|
-
process.env.QPQ_LOG_LEVEL = 'DEBUG';
|
|
656
|
-
|
|
657
|
-
// Or in config
|
|
658
|
-
import { defineLogging } from 'quidproquo-core';
|
|
659
|
-
|
|
660
|
-
export default [
|
|
661
|
-
defineLogging({
|
|
662
|
-
level: 'DEBUG',
|
|
663
|
-
pretty: true
|
|
664
|
-
}),
|
|
665
|
-
// ... other config
|
|
666
|
-
];
|
|
132
|
+
--language <typescript|javascript> skip the language prompt
|
|
133
|
+
--domain <domain> app domain (default: <app-name>.example.com)
|
|
134
|
+
--no-git skip git init
|
|
135
|
+
--no-install skip npm install (and the initial build)
|
|
667
136
|
```
|
|
668
137
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
```typescript
|
|
672
|
-
// src/debug.ts
|
|
673
|
-
import { createDevServer } from 'quidproquo-dev-server';
|
|
674
|
-
|
|
675
|
-
const server = createDevServer({
|
|
676
|
-
config,
|
|
677
|
-
port: 3000,
|
|
678
|
-
debug: {
|
|
679
|
-
logActions: true, // Log all actions
|
|
680
|
-
logResults: true, // Log action results
|
|
681
|
-
logDuration: true, // Log execution time
|
|
682
|
-
slowThreshold: 100, // Warn if action takes > 100ms
|
|
683
|
-
}
|
|
684
|
-
});
|
|
685
|
-
```
|
|
686
|
-
|
|
687
|
-
### Replay Executions
|
|
688
|
-
|
|
689
|
-
```typescript
|
|
690
|
-
// Capture execution
|
|
691
|
-
const execution = await runtime.execute(myStory, args, {
|
|
692
|
-
capture: true
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
// Save for debugging
|
|
696
|
-
fs.writeFileSync('execution.json', JSON.stringify(execution));
|
|
697
|
-
|
|
698
|
-
// Replay later
|
|
699
|
-
const executionLog = JSON.parse(fs.readFileSync('execution.json'));
|
|
700
|
-
const result = await qpqExecuteLog(executionLog, myStory);
|
|
701
|
-
```
|
|
702
|
-
|
|
703
|
-
## Best Practices
|
|
704
|
-
|
|
705
|
-
### 1. Keep Stories Pure
|
|
706
|
-
|
|
707
|
-
```typescript
|
|
708
|
-
// Good: Pure story with no side effects
|
|
709
|
-
function* calculatePrice(items: Item[]) {
|
|
710
|
-
const subtotal = items.reduce((sum, item) => sum + item.price, 0);
|
|
711
|
-
const tax = subtotal * 0.1;
|
|
712
|
-
return subtotal + tax;
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
// Bad: Side effects in story
|
|
716
|
-
function* calculatePrice(items: Item[]) {
|
|
717
|
-
console.log('Calculating...'); // Side effect!
|
|
718
|
-
const subtotal = items.reduce((sum, item) => sum + item.price, 0);
|
|
719
|
-
fs.writeFileSync('log.txt', `Subtotal: ${subtotal}`); // Side effect!
|
|
720
|
-
return subtotal * 1.1;
|
|
721
|
-
}
|
|
722
|
-
```
|
|
723
|
-
|
|
724
|
-
### 2. Use Composition
|
|
725
|
-
|
|
726
|
-
```typescript
|
|
727
|
-
// Compose smaller stories
|
|
728
|
-
function* validateEmail(email: string) {
|
|
729
|
-
if (!email.includes('@')) {
|
|
730
|
-
yield* askThrowError('INVALID_EMAIL', 'Invalid email format');
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
function* createUser(email: string, name: string) {
|
|
735
|
-
yield* validateEmail(email); // Reuse validation
|
|
736
|
-
// ... rest of creation logic
|
|
737
|
-
}
|
|
738
|
-
```
|
|
739
|
-
|
|
740
|
-
### 3. Handle Errors Gracefully
|
|
741
|
-
|
|
742
|
-
```typescript
|
|
743
|
-
function* safeOperation() {
|
|
744
|
-
const result = yield* askCatch(riskyOperation());
|
|
745
|
-
|
|
746
|
-
if (!result.success) {
|
|
747
|
-
// Log error
|
|
748
|
-
yield* askLogCreate('ERROR', result.error.errorText);
|
|
749
|
-
// Return default
|
|
750
|
-
return defaultValue;
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
return result.result;
|
|
754
|
-
}
|
|
755
|
-
```
|
|
756
|
-
|
|
757
|
-
### 4. Use TypeScript
|
|
758
|
-
|
|
759
|
-
```typescript
|
|
760
|
-
// Define types for your domain
|
|
761
|
-
interface User {
|
|
762
|
-
id: string;
|
|
763
|
-
email: string;
|
|
764
|
-
name: string;
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
// Type your stories
|
|
768
|
-
function* getUser(userId: string): Generator<any, User, any> {
|
|
769
|
-
const user = yield* askKeyValueStoreGet<User>('users', userId);
|
|
770
|
-
if (!user) {
|
|
771
|
-
yield* askThrowError('NOT_FOUND', 'User not found');
|
|
772
|
-
}
|
|
773
|
-
return user;
|
|
774
|
-
}
|
|
775
|
-
```
|
|
776
|
-
|
|
777
|
-
## Next Steps
|
|
778
|
-
|
|
779
|
-
Now that you have the basics, explore:
|
|
780
|
-
|
|
781
|
-
- [Core Concepts](./core-concepts.md) - Deep dive into the architecture
|
|
782
|
-
- [API Reference](./api/index.md) - Complete action reference
|
|
783
|
-
- [Use Cases](./use-cases.md) - Real-world examples
|
|
784
|
-
- [Architecture Overview](./architecture-overview.md) - Understanding the internals
|
|
785
|
-
|
|
786
|
-
## Getting Help
|
|
787
|
-
|
|
788
|
-
- **Documentation**: This site contains comprehensive documentation
|
|
789
|
-
- **GitHub Issues**: Report bugs or request features
|
|
790
|
-
- **Examples**: Check the examples directory in the repository
|
|
791
|
-
- **Community**: Join our Discord server (coming soon)
|
|
792
|
-
|
|
793
|
-
## Summary
|
|
794
|
-
|
|
795
|
-
You've learned how to:
|
|
796
|
-
- ✅ Install Quidproquo
|
|
797
|
-
- ✅ Create your first story
|
|
798
|
-
- ✅ Build a REST API
|
|
799
|
-
- ✅ Run locally with the dev server
|
|
800
|
-
- ✅ Deploy to various platforms
|
|
801
|
-
- ✅ Test your application
|
|
802
|
-
- ✅ Debug issues
|
|
138
|
+
Prefer plain JavaScript? `--language javascript` generates the same app with
|
|
139
|
+
type annotations stripped and JSX preserved — it runs on the same toolchain.
|
|
803
140
|
|
|
804
|
-
|
|
141
|
+
## Next steps
|
|
805
142
|
|
|
806
|
-
|
|
143
|
+
- [Core Concepts](./core-concepts.md) — stories, actions and processors in depth
|
|
144
|
+
- [Architecture Overview](./architecture-overview.md) — how the runtime executes a story
|
|
145
|
+
- [API Reference](./api/index.md) — the full action catalogue
|
|
@@ -179,7 +179,12 @@ Dive deep into the architecture that powers Quidproquo's platform independence a
|
|
|
179
179
|
|
|
180
180
|
## Next Steps
|
|
181
181
|
|
|
182
|
-
Ready to get started with Quidproquo?
|
|
182
|
+
Ready to get started with Quidproquo? One command scaffolds a complete,
|
|
183
|
+
running app:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
npx create-qpq-app myapp
|
|
187
|
+
```
|
|
183
188
|
|
|
184
189
|
<div className="row">
|
|
185
190
|
<div className="col col--6">
|
|
@@ -188,7 +193,7 @@ Ready to get started with Quidproquo?
|
|
|
188
193
|
<h3>🚀 Quick Start</h3>
|
|
189
194
|
</div>
|
|
190
195
|
<div className="card__body">
|
|
191
|
-
<p>
|
|
196
|
+
<p>Scaffold a five-service app that runs locally and deploys as a single docker image.</p>
|
|
192
197
|
</div>
|
|
193
198
|
<div className="card__footer">
|
|
194
199
|
<a href="/getting-started" className="button button--primary button--block">Get Started</a>
|
package/template/package.json
CHANGED
|
@@ -25,8 +25,9 @@
|
|
|
25
25
|
"lint:fix": "eslint . --fix",
|
|
26
26
|
"synth": "qpq synth",
|
|
27
27
|
"go:dev": "qpq go:dev",
|
|
28
|
-
"
|
|
28
|
+
"go:dev:api": "qpq go:dev:api",
|
|
29
29
|
"go:dev:web": "qpq go:dev:web",
|
|
30
|
+
"prep": "qpq prep",
|
|
30
31
|
"postinstall": "qpq hooks postinstall || true",
|
|
31
32
|
"go": "qpq go",
|
|
32
33
|
"publish": "qpq publish",
|