create-qpq-app 0.1.9 → 0.1.11
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/package.json +2 -2
- package/template/apps/qpqjs/deploy.config.json +2 -2
- package/template/apps/qpqjs/services/design/views/src/components/LandingPage/components/NavBar.tsx +5 -17
- package/template/apps/qpqjs/services/design/views/src/components/LandingPage/components/Packages.tsx +1 -2
- package/template/apps/qpqjs/services/design/views/src/components/LandingPage/landing.css +44 -0
- package/template/apps/qpqjs/services/shell/views/src/index.html +1 -1
- package/template/docusaurus/docs/actions/core/file/ask-file-generate-temporary-upload-secure-url.md +3 -1
- package/template/docusaurus/docs/actions/core/user-directory/ask-user-directory-revoke-refresh-token.md +56 -0
- package/template/docusaurus/docs/actions/core/user-directory/ask-user-directory-sign-out-user.md +52 -0
- package/template/docusaurus/docs/actions/features/event-doc/ask-apply-event-doc-event.md +6 -4
- package/template/docusaurus/docs/actions/features/event-doc/ask-apply-transient-event-doc-event.md +53 -0
- package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-generate-asset-upload-url.md +3 -1
- package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-read-identity.md +45 -0
- package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-read-state.md +46 -0
- package/template/docusaurus/docs/actions/features/web-socket-queue/ask-web-socket-queue-broadcast-message.md +50 -0
- package/template/docusaurus/docs/actions/features/web-socket-queue/ask-web-socket-queue-broadcast-service-updated.md +41 -0
- package/template/docusaurus/docs/actions/webserver/email/_category_.json +1 -0
- package/template/docusaurus/docs/actions/webserver/email/ask-email-send-email.md +85 -0
- package/template/docusaurus/docs/actions/webserver/service-function/ask-service-function-execute.md +19 -13
- package/template/docusaurus/docs/config/config-aws/email-sender-allow-list.md +45 -0
- package/template/docusaurus/docs/config/core/bundle-options.md +1 -1
- package/template/docusaurus/docs/config/core/user-directory.md +3 -1
- package/template/docusaurus/docs/config/features/admin-settings.md +5 -0
- package/template/docusaurus/docs/config/features/event-doc-routes.md +1 -0
- package/template/docusaurus/docs/config/features/event-doc.md +1 -0
- package/template/docusaurus/docs/config/features/web-socket-queue.md +4 -1
- package/template/docusaurus/docs/config/webserver/auth-system.md +9 -1
- package/template/docusaurus/docs/config/webserver/email-sender.md +43 -0
- package/template/docusaurus/docs/core-concepts.md +35 -1
- package/template/docusaurus/docs/eslint-plugin.md +86 -0
- package/template/docusaurus/docs/getting-started.md +3 -1
- package/template/docusaurus/sidebars.ts +1 -0
- package/template/docusaurus/src/css/custom.css +44 -0
- package/template/docusaurus/static/img/favicon.svg +6 -2
- package/template/docusaurus/static/img/qpq-logo.svg +4 -3
- package/template/package.json +2 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-qpq-app",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
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.11"
|
|
55
55
|
},
|
|
56
56
|
"bin": {
|
|
57
57
|
"create-qpq-app": "./lib/commonjs/bin/createQpqApp.js"
|
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
},
|
|
10
10
|
"staging": {
|
|
11
11
|
"platform": "aws",
|
|
12
|
-
"accountId": "
|
|
12
|
+
"accountId": "061039804449",
|
|
13
13
|
"region": "us-east-1"
|
|
14
14
|
},
|
|
15
15
|
"production": {
|
|
16
16
|
"platform": "aws",
|
|
17
17
|
"accountId": "761018864142",
|
|
18
|
-
"region": "
|
|
18
|
+
"region": "ap-southeast-2"
|
|
19
19
|
},
|
|
20
20
|
"local": {
|
|
21
21
|
"platform": "docker"
|
package/template/apps/qpqjs/services/design/views/src/components/LandingPage/components/NavBar.tsx
CHANGED
|
@@ -15,25 +15,13 @@ export function NavBar() {
|
|
|
15
15
|
<header className="nav">
|
|
16
16
|
<div className="nav__inner">
|
|
17
17
|
<a className="nav__brand" href="#top">
|
|
18
|
-
<svg aria-hidden="true" className="nav__mark" viewBox="
|
|
19
|
-
<rect
|
|
20
|
-
fill="rgba(34,211,238,0.08)"
|
|
21
|
-
height="26"
|
|
22
|
-
rx="7"
|
|
23
|
-
stroke="#22d3ee"
|
|
24
|
-
strokeWidth="1.6"
|
|
25
|
-
width="26"
|
|
26
|
-
x="3"
|
|
27
|
-
y="3"
|
|
28
|
-
/>
|
|
18
|
+
<svg aria-hidden="true" className="nav__mark" viewBox="24.4 24.4 465.3 465.3">
|
|
29
19
|
<path
|
|
30
|
-
d="
|
|
31
|
-
fill="
|
|
32
|
-
stroke="#8df6ff"
|
|
33
|
-
strokeLinecap="round"
|
|
34
|
-
strokeLinejoin="round"
|
|
35
|
-
strokeWidth="2.2"
|
|
20
|
+
d="M271.42,489.64h-10.72c-128.68,0-235.74-102.93-236.29-231.61S128.97,23.86,258.03,24.41c128.68.55,231.6,107.61,231.6,236.29v11.61c0,.09-.07.16-.16.16h-109.69c-.09,0-.16-.07-.16-.16v-12.61c0-65.9-53.82-123.71-119.7-125.24-69.89-1.62-127.08,55.57-125.46,125.46,1.53,65.89,59.34,119.71,125.24,119.71h11.72c.09,0,.16.07.16.16v109.69c0,.09-.07.16-.16.16Z"
|
|
21
|
+
fill="#22d3ee"
|
|
36
22
|
/>
|
|
23
|
+
<rect fill="#8df6ff" height="106.91" width="106.91" x="268.31" y="273.22" />
|
|
24
|
+
<rect fill="#8df6ff" height="106.91" width="106.91" x="375.62" y="380.53" />
|
|
37
25
|
</svg>
|
|
38
26
|
<span className="nav__name">
|
|
39
27
|
quid<span className="nav__name-accent">pro</span>quo
|
package/template/apps/qpqjs/services/design/views/src/components/LandingPage/components/Packages.tsx
CHANGED
|
@@ -40,8 +40,7 @@ export function Packages() {
|
|
|
40
40
|
<p className="section__kicker">the grid</p>
|
|
41
41
|
<h2 className="section__title">A package for every layer</h2>
|
|
42
42
|
<p className="section__sub">
|
|
43
|
-
|
|
44
|
-
in.
|
|
43
|
+
Every layer is its own package, so you only pull in what you need.
|
|
45
44
|
</p>
|
|
46
45
|
|
|
47
46
|
<div className="package-groups">
|
|
@@ -355,6 +355,7 @@ em {
|
|
|
355
355
|
}
|
|
356
356
|
|
|
357
357
|
.nav__name {
|
|
358
|
+
position: relative;
|
|
358
359
|
font-family: var(--font-display);
|
|
359
360
|
font-size: 19px;
|
|
360
361
|
font-weight: 600;
|
|
@@ -366,6 +367,49 @@ em {
|
|
|
366
367
|
color: var(--cyan);
|
|
367
368
|
}
|
|
368
369
|
|
|
370
|
+
/* Glint overlay: the same glyphs stacked over the title, transparent except
|
|
371
|
+
for a narrow band clipped to the letterforms. It rests off-canvas, then
|
|
372
|
+
makes one eased sweep across per cycle. The band is two-tone so it reads
|
|
373
|
+
on both letter colors: the white half is invisible on the near-white
|
|
374
|
+
quid/quo but lights up the cyan pro, and the cyan half does the reverse.
|
|
375
|
+
The "/ ''" marks the duplicated text as decorative; browsers without that
|
|
376
|
+
syntax skip the overlay entirely, which just means no shimmer. */
|
|
377
|
+
.nav__name::after {
|
|
378
|
+
content: 'quidproquo' / '';
|
|
379
|
+
position: absolute;
|
|
380
|
+
inset: 0;
|
|
381
|
+
color: transparent;
|
|
382
|
+
background-image: linear-gradient(
|
|
383
|
+
105deg,
|
|
384
|
+
rgba(255, 255, 255, 0) 38%,
|
|
385
|
+
rgba(255, 255, 255, 0.85) 45%,
|
|
386
|
+
rgba(34, 211, 238, 0.85) 53%,
|
|
387
|
+
rgba(34, 211, 238, 0) 60%
|
|
388
|
+
);
|
|
389
|
+
background-size: 250% 100%;
|
|
390
|
+
background-repeat: no-repeat;
|
|
391
|
+
background-position: 130% 0;
|
|
392
|
+
-webkit-background-clip: text;
|
|
393
|
+
background-clip: text;
|
|
394
|
+
animation: nav-name-shine 7s ease-in-out infinite;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
@keyframes nav-name-shine {
|
|
398
|
+
0%,
|
|
399
|
+
72% {
|
|
400
|
+
background-position: 130% 0;
|
|
401
|
+
}
|
|
402
|
+
100% {
|
|
403
|
+
background-position: -130% 0;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
@media (prefers-reduced-motion: reduce) {
|
|
408
|
+
.nav__name::after {
|
|
409
|
+
animation: none;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
369
413
|
.nav__links {
|
|
370
414
|
display: flex;
|
|
371
415
|
gap: 26px;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<link
|
|
6
6
|
rel="icon"
|
|
7
|
-
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect x='
|
|
7
|
+
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect x='1' y='1' width='30' height='30' rx='6' fill='%23071018'/%3E%3Cg transform='translate(4.5 4.5) scale(0.0494) translate(-24.4 -24.4)'%3E%3Cpath fill='%2322d3ee' d='M271.42,489.64h-10.72c-128.68,0-235.74-102.93-236.29-231.61S128.97,23.86,258.03,24.41c128.68.55,231.6,107.61,231.6,236.29v11.61c0,.09-.07.16-.16.16h-109.69c-.09,0-.16-.07-.16-.16v-12.61c0-65.9-53.82-123.71-119.7-125.24-69.89-1.62-127.08,55.57-125.46,125.46,1.53,65.89,59.34,119.71,125.24,119.71h11.72c.09,0,.16.07.16.16v109.69c0,.09-.07.16-.16.16Z'/%3E%3Crect fill='%238df6ff' x='268.31' y='273.22' width='106.91' height='106.91'/%3E%3Crect fill='%238df6ff' x='375.62' y='380.53' width='106.91' height='106.91'/%3E%3C/g%3E%3C/svg%3E"
|
|
8
8
|
/>
|
|
9
9
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
10
10
|
<meta
|
package/template/docusaurus/docs/actions/core/file/ask-file-generate-temporary-upload-secure-url.md
CHANGED
|
@@ -8,7 +8,7 @@ description: Generate a time-limited, signed URL that lets a client upload a fil
|
|
|
8
8
|
Generates a temporary, signed URL that lets a client **upload** a single file directly to a [storage drive](../../../config/core/storage-drive.md). The bytes go straight to storage instead of through your service, which is ideal for large uploads from a browser. The URL grants write access to just that one path and stops working once it expires.
|
|
9
9
|
|
|
10
10
|
- **Action type:** `FileActionType.GenerateTemporaryUploadSecureUrl`
|
|
11
|
-
- **On AWS:** returns a SigV4 presigned S3 `PutObject` URL for the object. The expiry cannot exceed **7 days** — the maximum lifetime of a SigV4 presigned URL. If `contentType`
|
|
11
|
+
- **On AWS:** returns a SigV4 presigned S3 `PutObject` URL for the object. The expiry cannot exceed **7 days** — the maximum lifetime of a SigV4 presigned URL. If `contentType` and/or `contentDisposition` are supplied they are bound into the signature, so the upload's `Content-Type` / `Content-Disposition` headers must match, and S3 stores them so later downloads serve the object with the same headers.
|
|
12
12
|
|
|
13
13
|
```typescript
|
|
14
14
|
import { askFileGenerateTemporaryUploadSecureUrl } from 'quidproquo-core';
|
|
@@ -34,6 +34,7 @@ function* askFileGenerateTemporaryUploadSecureUrl(
|
|
|
34
34
|
expirationMs: number,
|
|
35
35
|
advancedOptions?: {
|
|
36
36
|
contentType?: string;
|
|
37
|
+
contentDisposition?: string;
|
|
37
38
|
},
|
|
38
39
|
scope?: string,
|
|
39
40
|
): AskResponse<string>;
|
|
@@ -54,6 +55,7 @@ function* askFileGenerateTemporaryUploadSecureUrl(
|
|
|
54
55
|
| Property | Type | Default | Description |
|
|
55
56
|
| --- | --- | --- | --- |
|
|
56
57
|
| `contentType` | `string` | – | Pins the `Content-Type` of the upload into the signature. When set, the client must send a matching `Content-Type` header or the `PUT` is rejected. |
|
|
58
|
+
| `contentDisposition` | `string` | – | Pins the `Content-Disposition` of the upload into the signature (e.g. `inline` so a stored PDF previews in an `<iframe>` instead of force-downloading). When set, the client must send a matching `Content-Disposition` header, and S3 stores it so later downloads serve it back. |
|
|
57
59
|
|
|
58
60
|
## Returns
|
|
59
61
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: askUserDirectoryRevokeRefreshToken
|
|
3
|
+
description: Revoke a single refresh token — the "log out this device" primitive.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# askUserDirectoryRevokeRefreshToken
|
|
7
|
+
|
|
8
|
+
Revokes a single refresh token (and every access token minted from it) against a [user directory](../../../config/core/user-directory.md). This is the "log out this device" primitive: other sessions belonging to the same user, holding different refresh tokens, are unaffected.
|
|
9
|
+
|
|
10
|
+
- **Action type:** `UserDirectoryActionType.RevokeRefreshToken`
|
|
11
|
+
- **On AWS:** issues Cognito `RevokeToken`. Requires token revocation to be enabled on the app client (the CDK default); a disabled client surfaces as an unmapped error.
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { askUserDirectoryRevokeRefreshToken } from 'quidproquo-core';
|
|
15
|
+
|
|
16
|
+
export function* askLogOutThisDevice(refreshToken: string) {
|
|
17
|
+
yield* askUserDirectoryRevokeRefreshToken('app-users', refreshToken);
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Signature
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
function* askUserDirectoryRevokeRefreshToken(
|
|
25
|
+
userDirectoryName: string,
|
|
26
|
+
refreshToken: string,
|
|
27
|
+
): AskResponse<void>;
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Parameters
|
|
31
|
+
|
|
32
|
+
| Parameter | Type | Description |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| `userDirectoryName` | `string` | Name of the directory — must match a directory declared with [defineUserDirectory](../../../config/core/user-directory.md). |
|
|
35
|
+
| `refreshToken` | `string` | The refresh token to revoke (from `AuthenticationInfo.refreshToken`). |
|
|
36
|
+
|
|
37
|
+
## Returns
|
|
38
|
+
|
|
39
|
+
`void` — the story resumes once the token has been revoked.
|
|
40
|
+
|
|
41
|
+
## Errors
|
|
42
|
+
|
|
43
|
+
| Error | Meaning |
|
|
44
|
+
| --- | --- |
|
|
45
|
+
| `UserDirectoryRevokeRefreshTokenErrorTypeEnum.Unauthorized` | The refresh token is invalid or already revoked, or revocation is disabled on the app client. |
|
|
46
|
+
| `UserDirectoryRevokeRefreshTokenErrorTypeEnum.LimitExceeded` | Too many attempts; back off and retry later. |
|
|
47
|
+
|
|
48
|
+
## Notes
|
|
49
|
+
|
|
50
|
+
Access tokens already minted from the revoked refresh token remain valid until they expire — they're stateless JWTs and can't be individually invalidated. Keep `accessTokenValidityMinutes` (see [defineUserDirectory](../../../config/core/user-directory.md#parameters)) short to shrink that window.
|
|
51
|
+
|
|
52
|
+
## Related
|
|
53
|
+
|
|
54
|
+
- [askUserDirectorySignOutUser](./ask-user-directory-sign-out-user.md) — revokes **every** refresh token for the user, not just one.
|
|
55
|
+
- [askUserDirectoryRefreshToken](./ask-user-directory-refresh-token.md) — exchanges a refresh token for fresh tokens.
|
|
56
|
+
- [defineUserDirectory](../../../config/core/user-directory.md) — declares the directory.
|
package/template/docusaurus/docs/actions/core/user-directory/ask-user-directory-sign-out-user.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: askUserDirectorySignOutUser
|
|
3
|
+
description: Globally sign a user out by revoking every refresh token issued to them.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# askUserDirectorySignOutUser
|
|
7
|
+
|
|
8
|
+
Globally signs a user out: revokes **every** refresh token issued to the owner of the supplied access token, against a [user directory](../../../config/core/user-directory.md). Use this for a "sign out of all devices" action or as a compromise response, since a stolen/persisted refresh token stops working for every session at once.
|
|
9
|
+
|
|
10
|
+
- **Action type:** `UserDirectoryActionType.SignOutUser`
|
|
11
|
+
- **On AWS:** issues Cognito `GlobalSignOut`. Authorized by the access token itself — no user directory name or IAM is needed to identify the user, Cognito resolves it from the token.
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { askUserDirectorySignOutUser } from 'quidproquo-core';
|
|
15
|
+
|
|
16
|
+
export function* askLogOutEverywhere(accessToken: string) {
|
|
17
|
+
yield* askUserDirectorySignOutUser(accessToken);
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Signature
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
function* askUserDirectorySignOutUser(accessToken: string): AskResponse<void>;
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Parameters
|
|
28
|
+
|
|
29
|
+
| Parameter | Type | Description |
|
|
30
|
+
| --- | --- | --- |
|
|
31
|
+
| `accessToken` | `string` | The signed-in user's access token, identifying whose sessions to sign out. |
|
|
32
|
+
|
|
33
|
+
## Returns
|
|
34
|
+
|
|
35
|
+
`void` — the story resumes once every session has been revoked.
|
|
36
|
+
|
|
37
|
+
## Errors
|
|
38
|
+
|
|
39
|
+
| Error | Meaning |
|
|
40
|
+
| --- | --- |
|
|
41
|
+
| `UserDirectorySignOutUserErrorTypeEnum.Unauthorized` | The access token is missing, invalid, or expired — there is nothing to revoke. |
|
|
42
|
+
| `UserDirectorySignOutUserErrorTypeEnum.LimitExceeded` | Too many attempts; back off and retry later. |
|
|
43
|
+
|
|
44
|
+
## Notes
|
|
45
|
+
|
|
46
|
+
Access tokens already minted remain valid until they expire — they're stateless JWTs and can't be individually invalidated. Keep `accessTokenValidityMinutes` (see [defineUserDirectory](../../../config/core/user-directory.md#parameters)) short to shrink that window.
|
|
47
|
+
|
|
48
|
+
## Related
|
|
49
|
+
|
|
50
|
+
- [askUserDirectoryRevokeRefreshToken](./ask-user-directory-revoke-refresh-token.md) — revokes only a single session, leaving the user's other devices signed in.
|
|
51
|
+
- [askUserDirectoryReadAccessToken](./ask-user-directory-read-access-token.md) / [askUserDirectoryDecodeAccessToken](./ask-user-directory-decode-access-token.md) — inspect an access token.
|
|
52
|
+
- [defineUserDirectory](../../../config/core/user-directory.md) — declares the directory.
|
|
@@ -15,22 +15,24 @@ A **purely declarative** way to apply an event-doc event: the story yields an `e
|
|
|
15
15
|
import { askApplyEventDocEvent } from 'quidproquo-features';
|
|
16
16
|
|
|
17
17
|
export function* askTenantSetBrand(data: TenantSetBrandData) {
|
|
18
|
-
yield* askApplyEventDocEvent(TenantEffect.setBrand, data);
|
|
18
|
+
yield* askApplyEventDocEvent<SetBrandEffect>(TenantEffect.setBrand, data);
|
|
19
19
|
}
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
## Signature
|
|
23
23
|
|
|
24
24
|
```typescript
|
|
25
|
-
function* askApplyEventDocEvent(eventType:
|
|
25
|
+
function* askApplyEventDocEvent<E extends Effect<string, any>>(eventType: E['type'], data: E['payload']): AskResponse<void>;
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
+
Typed like [askStateDispatchEffect](../../core/state/ask-state-dispatch.md#effect-and-askstatedispatcheffect): an event-doc event IS a special kind of effect (`Effect<type, data>`), so an action creator passes its effect type as `E` and gets `data` checked against it.
|
|
29
|
+
|
|
28
30
|
## Parameters
|
|
29
31
|
|
|
30
32
|
| Parameter | Type | Description |
|
|
31
33
|
| --- | --- | --- |
|
|
32
|
-
| `eventType` | `
|
|
33
|
-
| `data` | `
|
|
34
|
+
| `eventType` | `E['type']` | The effect/event type discriminant, matched by the reducer that folds the document and by the registered processor. |
|
|
35
|
+
| `data` | `E['payload']` | The typed domain data for the event, checked against `E`, opaque to this action itself. |
|
|
34
36
|
|
|
35
37
|
## Returns
|
|
36
38
|
|
package/template/docusaurus/docs/actions/features/event-doc/ask-apply-transient-event-doc-event.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: askApplyTransientEventDocEvent
|
|
3
|
+
description: Declaratively apply an event-doc event into a never-saved transient group, dropped wholesale by key instead of persisted.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# askApplyTransientEventDocEvent
|
|
7
|
+
|
|
8
|
+
The never-saved sibling of [askApplyEventDocEvent](./ask-apply-event-doc-event.md): the story yields an `eventType` and its `data` under a `transientKey`, and the event lands in a transient group that is never persisted, only dropped wholesale by that key. It is the event-sourced way to represent observations that must not survive past their source, such as websocket progress messages, where dropping the key (usually a connection id) reverts the folded view reactively instead of leaving a stuck UI state behind.
|
|
9
|
+
|
|
10
|
+
- **Action type:** `EventDocActionType.ApplyTransientEvent`
|
|
11
|
+
- Transient applies are client-runtime-only by definition: a server "authors" them only via messages a client story processes, never a backend `ApplyTransientEvent` processor. An unbound or backend apply fails loudly, the same as an unbound `ApplyEvent`.
|
|
12
|
+
- The version stamped on the resulting event is not passed by the caller — the binding that applies it stamps its own configured schema version, the same as `askApplyEventDocEvent`.
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
import { askApplyTransientEventDocEvent } from 'quidproquo-features';
|
|
16
|
+
|
|
17
|
+
export function* askZipReportProgress(connectionId: string, data: ZipProgressData) {
|
|
18
|
+
yield* askApplyTransientEventDocEvent<ZipProgressEffect>(connectionId, ZipEffect.Progress, data);
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Signature
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
function* askApplyTransientEventDocEvent<E extends Effect<string, any>>(
|
|
26
|
+
transientKey: string,
|
|
27
|
+
eventType: E['type'],
|
|
28
|
+
data: E['payload'],
|
|
29
|
+
): AskResponse<void>;
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Typed like [askApplyEventDocEvent](./ask-apply-event-doc-event.md): an event-doc event IS a special kind of effect (`Effect<type, data>`), so an action creator passes its effect type as `E` and gets `data` checked against it.
|
|
33
|
+
|
|
34
|
+
## Parameters
|
|
35
|
+
|
|
36
|
+
| Parameter | Type | Description |
|
|
37
|
+
| --- | --- | --- |
|
|
38
|
+
| `transientKey` | `string` | Names the drop unit for this event, usually a websocket connection id. Dropping a key clears every event committed under it. |
|
|
39
|
+
| `eventType` | `E['type']` | The effect/event type discriminant, matched by the reducer that folds the document. |
|
|
40
|
+
| `data` | `E['payload']` | The typed domain data for the event, checked against `E`, opaque to this action itself. |
|
|
41
|
+
|
|
42
|
+
## Returns
|
|
43
|
+
|
|
44
|
+
`AskResponse<void>` — applying a transient event never returns a value; a binding that fails to apply it surfaces the failure as its own runtime's error state rather than throwing back through this call.
|
|
45
|
+
|
|
46
|
+
## Notes
|
|
47
|
+
|
|
48
|
+
- This is the contract only — it carries no default behavior on its own. In the event-doc workspace, the binding registered for a slot routes `ApplyTransientEvent` into that slot's transient group under `transientKey`; outside a workspace bind, calling it fails loudly.
|
|
49
|
+
- Transient events never influence the persistable log or validation: they are excluded from the log every save and validator reads, and are dropped wholesale by `transientKey` rather than removed one at a time.
|
|
50
|
+
|
|
51
|
+
## Related
|
|
52
|
+
|
|
53
|
+
- [askApplyEventDocEvent](./ask-apply-event-doc-event.md) — the persisted sibling this action mirrors.
|
|
@@ -14,7 +14,7 @@ These three helpers cover the two flows: presigned URLs for a browser to upload/
|
|
|
14
14
|
|
|
15
15
|
## askEventDocGenerateAssetUploadUrl
|
|
16
16
|
|
|
17
|
-
Mints a guid and a short-lived presigned `PUT` URL the client uploads the bytes to. Returns the guid (as `assetId`) so the caller can record it in a domain event. This is one complete storage operation — no bytes pass through your service.
|
|
17
|
+
Mints a guid and a short-lived presigned `PUT` URL the client uploads the bytes to. Returns the guid (as `assetId`) so the caller can record it in a domain event. This is one complete storage operation — no bytes pass through your service. An optional `contentDisposition` is baked into the upload (the client must `PUT` the matching header) so the stored object serves with it, e.g. `inline` so a rendered PDF previews in an `<iframe>` instead of downloading.
|
|
18
18
|
|
|
19
19
|
```typescript
|
|
20
20
|
import { askEventDocGenerateAssetUploadUrl } from 'quidproquo-features';
|
|
@@ -32,6 +32,7 @@ export function* startImageUpload(docId: string) {
|
|
|
32
32
|
function* askEventDocGenerateAssetUploadUrl(
|
|
33
33
|
docId: string,
|
|
34
34
|
contentType: string,
|
|
35
|
+
contentDisposition?: string,
|
|
35
36
|
): AskResponse<EventDocAssetUploadUrl>;
|
|
36
37
|
```
|
|
37
38
|
|
|
@@ -41,6 +42,7 @@ function* askEventDocGenerateAssetUploadUrl(
|
|
|
41
42
|
| --- | --- | --- |
|
|
42
43
|
| `docId` | `string` | The document the asset belongs to — determines the `<docId>/assets/<guid>` blob key. |
|
|
43
44
|
| `contentType` | `string` | The MIME type the client will upload; pinned into the presigned `PUT` URL. |
|
|
45
|
+
| `contentDisposition` | `string` | Optional. Pinned into the presigned `PUT` URL so the stored asset serves with it, e.g. `inline` to preview instead of force-download. |
|
|
44
46
|
|
|
45
47
|
### Returns
|
|
46
48
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: askEventDocReadIdentity
|
|
3
|
+
description: Declaratively read where an event-doc's own document lives (serviceName/basePath/id), for verbs that build links relative to their own doc.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# askEventDocReadIdentity
|
|
7
|
+
|
|
8
|
+
The address sibling of [askEventDocReadState](./ask-event-doc-read-state.md): the story yields no payload at all, and a registered processor answers with the doc's own identity — `serviceName`, `basePath`, and `id`. Verbs written in terms of it (for example, building an `EventDocLink` relative to their own doc) stay workspace-blind, resolving whichever doc the processor is bound to.
|
|
9
|
+
|
|
10
|
+
- **Action type:** `EventDocActionType.ReadIdentity`
|
|
11
|
+
- **quidproquo-features ships no default processor** for this action. Calling it with no processor registered fails loudly, the same as an unbound [askEventDocReadState](./ask-event-doc-read-state.md).
|
|
12
|
+
- Returns `null` until the doc has an identity to report: before the workspace initializes the slot, and always for an unsaved doc (it has no server-backed identity to resolve).
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
import { askEventDocReadIdentity } from 'quidproquo-features';
|
|
16
|
+
|
|
17
|
+
export function* askTemplateBuildSelfLink() {
|
|
18
|
+
const identity = yield* askEventDocReadIdentity();
|
|
19
|
+
return identity ? { serviceName: identity.serviceName, basePath: identity.basePath, id: identity.id } : null;
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Signature
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
function* askEventDocReadIdentity(): AskResponse<Nullable<EventDocWorkspaceDocumentIdentity>>;
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Parameters
|
|
30
|
+
|
|
31
|
+
None — WHICH doc's identity to read is the answering processor's ambient context, the same as `askEventDocReadState`.
|
|
32
|
+
|
|
33
|
+
## Returns
|
|
34
|
+
|
|
35
|
+
`AskResponse<Nullable<EventDocWorkspaceDocumentIdentity>>` — `{ serviceName, basePath, id }`, concretely typed since every doc's identity has the same shape. `null` until the doc is bound to a real document.
|
|
36
|
+
|
|
37
|
+
## Notes
|
|
38
|
+
|
|
39
|
+
- This is the contract only — it carries no default behavior. A runtime that answers `askEventDocReadState` for a doc should also answer `askEventDocReadIdentity` for it.
|
|
40
|
+
- In the event-doc workspace, the binding registered for a slot answers with `state.slots[slotKey].documentIdentity`, so this reflects the same identity `askInit` seeded for the slot.
|
|
41
|
+
|
|
42
|
+
## Related
|
|
43
|
+
|
|
44
|
+
- [askEventDocReadState](./ask-event-doc-read-state.md) — the content sibling: what the doc currently holds, rather than where it lives.
|
|
45
|
+
- [askApplyEventDocEvent](./ask-apply-event-doc-event.md) — the write action this doc's verbs typically pair with.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: askEventDocReadState
|
|
3
|
+
description: Declaratively read an event-doc's current folded state, leaving WHICH doc and HOW it is resolved to a registered processor.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# askEventDocReadState
|
|
7
|
+
|
|
8
|
+
The read counterpart of [askApplyEventDocEvent](./ask-apply-event-doc-event.md): the story yields no payload at all, and a registered processor answers with the doc's current folded state. Because it has no target of its own, verbs written in terms of it (a domain's read-to-derive-a-write action creators) run unchanged wherever a processor for it is registered, resolving whichever doc the processor is bound to.
|
|
9
|
+
|
|
10
|
+
- **Action type:** `EventDocActionType.ReadState`
|
|
11
|
+
- **quidproquo-features ships no default processor** for this action. Calling it with no processor registered fails loudly, the same as an unbound [askApplyEventDocEvent](./ask-apply-event-doc-event.md).
|
|
12
|
+
- Returns `unknown` — the raw action can't know a slot's view type. Call it through a per-doc `createEventDocStateReader<TView>()` instead of directly, for a typed result.
|
|
13
|
+
- In the event-doc workspace, the binding registered for a slot answers with the doc's current memoized view (history + pending folded and migrated to latest), so a commit earlier in the same story is visible to a read that follows it.
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { askEventDocReadState } from 'quidproquo-features';
|
|
17
|
+
|
|
18
|
+
export function* askTemplateAppendLine(line: string) {
|
|
19
|
+
const template = (yield* askEventDocReadState()) as TemplateState;
|
|
20
|
+
yield* askApplyEventDocEvent<AppendLineEffect>(TemplateEffect.AppendLine, { line: template.body ? `${template.body}\n${line}` : line });
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Signature
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
function* askEventDocReadState(): AskResponse<unknown>;
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Parameters
|
|
31
|
+
|
|
32
|
+
None — WHICH doc to read is the answering processor's ambient context, the same as `askApplyEventDocEvent`'s target.
|
|
33
|
+
|
|
34
|
+
## Returns
|
|
35
|
+
|
|
36
|
+
`AskResponse<unknown>` — the doc's current folded state, untyped at this layer. Use `createEventDocStateReader<TView>()` to mint a typed `askRead<Doc>()` for a specific doc instead of casting the result by hand at every call site.
|
|
37
|
+
|
|
38
|
+
## Notes
|
|
39
|
+
|
|
40
|
+
- This is the contract only — it carries no default behavior. A runtime that answers `askApplyEventDocEvent` for a doc should also answer `askEventDocReadState` for it, so read-to-derive-a-write verbs work the same way commits do.
|
|
41
|
+
- `createEventDocDefinition` mints the doc's fold config and api together; pair it with a standalone `createEventDocStateReader<TView>()` for the doc's typed read verb.
|
|
42
|
+
|
|
43
|
+
## Related
|
|
44
|
+
|
|
45
|
+
- [askApplyEventDocEvent](./ask-apply-event-doc-event.md) — the write counterpart this action mirrors.
|
|
46
|
+
- [askEventDocReadIdentity](./ask-event-doc-read-identity.md) — the address sibling: where the doc lives, rather than what it currently holds.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: askWebSocketQueueBroadcastMessage
|
|
3
|
+
description: Push a server-initiated message to every live connection on a WebSocket queue's API.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# askWebSocketQueueBroadcastMessage
|
|
7
|
+
|
|
8
|
+
Sends a message to **every live connection** on a named [WebSocket queue](../../../config/features/web-socket-queue.md) API. Unlike a connection-scoped send, this does not need a websocket-triggered context — the caller names the `apiName` explicitly, so any story (a storage event, a queue handler, a cron job) can push to every connected frontend. Connections whose socket died without their `onDisconnect` firing are skipped and their stale record is cleaned up automatically; a single dead connection never aborts the broadcast.
|
|
9
|
+
|
|
10
|
+
```typescript
|
|
11
|
+
import { askWebSocketQueueBroadcastMessage } from 'quidproquo-features';
|
|
12
|
+
|
|
13
|
+
interface Announcement {
|
|
14
|
+
type: 'Announcement';
|
|
15
|
+
payload: { text: string };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function* askAnnounce(text: string) {
|
|
19
|
+
yield* askWebSocketQueueBroadcastMessage<Announcement>('api', {
|
|
20
|
+
type: 'Announcement',
|
|
21
|
+
payload: { text },
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Signature
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
function* askWebSocketQueueBroadcastMessage<E extends AnyWebSocketQueueEventMessage>(
|
|
30
|
+
websocketApiName: string,
|
|
31
|
+
message: E,
|
|
32
|
+
): AskResponse<void>;
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Parameters
|
|
36
|
+
|
|
37
|
+
| Parameter | Type | Description |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| `websocketApiName` | `string` | The `apiName` of the target [defineWebSocketQueue](../../../config/features/web-socket-queue.md) — every connection currently registered against this API receives the message. |
|
|
40
|
+
| `message` | `E extends AnyWebSocketQueueEventMessage` | The typed event message to send, e.g. `{ type, payload }`. |
|
|
41
|
+
|
|
42
|
+
## Returns
|
|
43
|
+
|
|
44
|
+
`void`
|
|
45
|
+
|
|
46
|
+
## Related
|
|
47
|
+
|
|
48
|
+
- [askWebSocketQueueBroadcastServiceUpdated](./ask-web-socket-queue-broadcast-service-updated.md) — a typed wrapper around this for the built-in `ServiceUpdated` message.
|
|
49
|
+
- [defineWebSocketQueue](../../../config/features/web-socket-queue.md) — declares the WebSocket queue API this broadcasts on.
|
|
50
|
+
- [askServiceRequest](./ask-service-request.md) — an RPC-style request/response over the same queue, addressed to one service rather than broadcast to every connection.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: askWebSocketQueueBroadcastServiceUpdated
|
|
3
|
+
description: Tell every connected frontend that a service's deployed artifacts changed, so it can offer a module reload.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# askWebSocketQueueBroadcastServiceUpdated
|
|
7
|
+
|
|
8
|
+
Broadcasts a `ServiceUpdated` message to every live connection on a [WebSocket queue](../../../config/features/web-socket-queue.md) API, naming the service whose deployed artifacts changed (for example, a new views bundle going live). A typed wrapper around [askWebSocketQueueBroadcastMessage](./ask-web-socket-queue-broadcast-message.md) for this one built-in message type.
|
|
9
|
+
|
|
10
|
+
```typescript
|
|
11
|
+
import { askWebSocketQueueBroadcastServiceUpdated } from 'quidproquo-features';
|
|
12
|
+
|
|
13
|
+
export function* askNotifyDeploy(serviceName: string) {
|
|
14
|
+
yield* askWebSocketQueueBroadcastServiceUpdated('api', serviceName);
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Signature
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
function* askWebSocketQueueBroadcastServiceUpdated(
|
|
22
|
+
websocketApiName: string,
|
|
23
|
+
serviceName: string,
|
|
24
|
+
): AskResponse<void>;
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Parameters
|
|
28
|
+
|
|
29
|
+
| Parameter | Type | Description |
|
|
30
|
+
| --- | --- | --- |
|
|
31
|
+
| `websocketApiName` | `string` | The `apiName` of the target [defineWebSocketQueue](../../../config/features/web-socket-queue.md) — every connection currently registered against this API receives the message. |
|
|
32
|
+
| `serviceName` | `string` | The qpq service (module) whose deployed artifacts changed. Sent as `payload.serviceName` on a `WebSocketQueueServerMessageEventType.ServiceUpdated` message. |
|
|
33
|
+
|
|
34
|
+
## Returns
|
|
35
|
+
|
|
36
|
+
`void`
|
|
37
|
+
|
|
38
|
+
## Related
|
|
39
|
+
|
|
40
|
+
- [askWebSocketQueueBroadcastMessage](./ask-web-socket-queue-broadcast-message.md) — the untyped broadcast this wraps.
|
|
41
|
+
- [defineWebSocketQueue](../../../config/features/web-socket-queue.md) — declares the WebSocket queue API this broadcasts on.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "label": "Email", "link": { "type": "generated-index", "description": "Actions for sending email from a service." } }
|