create-qpq-app 0.1.5 → 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/008_transpileToJavaScript.js +108 -5
- package/lib/commonjs/steps/008_transpileToJavaScript.js.map +1 -1
- package/lib/commonjs/steps/013_printNextSteps.js +1 -1
- package/lib/commonjs/steps/013_printNextSteps.js.map +1 -1
- package/lib/esm/steps/008_transpileToJavaScript.js +104 -5
- package/lib/esm/steps/008_transpileToJavaScript.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 +6 -4
- 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/apps/todo/packages/service-utils/src/defineTodoService.ts +15 -5
- 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
- package/template/tsconfig.base.json +4 -0
|
@@ -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
|