create-qpq-app 0.1.8 → 0.1.9

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-qpq-app",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
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.8"
54
+ "quidproquo-tsconfig": "0.1.9"
55
55
  },
56
56
  "bin": {
57
57
  "create-qpq-app": "./lib/commonjs/bin/createQpqApp.js"
@@ -62,6 +62,7 @@ The single `options` argument is an `EventDocRoutesOptions`:
62
62
  | `eventRenderer` | `string` | – | Name of a registered inline function. When set, a `GET {basePath}/{id}/render` route is mounted; it invokes the renderer with the document's full `{ events }` log, which folds + renders to HTML. |
63
63
  | `onPublish` | `string` | – | Name of a registered inline function. When set, every successful append of a Publish event invokes it with `{ docId, event, summary }`, after the event is durably written and the summary re-derived. This is the seam for syncing a folded document into a materialized read model. Errors propagate to the caller: the event has landed but the side effect did not, so the caller learns the read model may be stale. |
64
64
  | `scopeResolver` | `string` | – | Name of a registered inline function. When set, every route invokes it with `{ event }` before running; a non-null result becomes the ambient storage scope for the whole request, transparently partitioning the collection's stores and assets (e.g. per-tenant). Null means unscoped. Omit for collections that never partition. |
65
+ | `excludeRoutes` | `EventDocRouteName[]` | `[]` | Route names to leave unmounted (`'list' \| 'get' \| 'listEvents' \| 'render' \| 'create' \| 'appendEvent' \| 'createAsset' \| 'getAsset' \| 'remove'`). For a collection that must own one of these itself instead of using the stock behavior — e.g. a `create` that must also perform some side effect the stock controller doesn't know about. |
65
66
 
66
67
  ### `RouteAuthSettings`
67
68
 
@@ -12,8 +12,8 @@ Wires up **everything for org/tenant support**, declared identically in every se
12
12
  - Everything else, gated to the owner's deploy only via [defineServiceSettings](../core/service-settings.md):
13
13
  - The tenant stores ([defineTenantStores](./tenant-stores.md)): the tenant event-doc collection plus the materialized record table.
14
14
  - The publish-to-record-store sync: an inline function (`askTenantOnPublish`) that runs on every published tenant document, re-folds the full event log, and upserts the resulting `TenantRecord`. It is a plain upsert of the fold result, so publish retries and repair re-runs are safe.
15
- - The generic event-doc CRUD under `{basePath}/docs` ([defineEventDocRoutes](./event-doc-routes.md) for the `tenants` store, `tenant` type, with the publish sync wired in as `onPublish`).
16
- - The tenant-specific routes at `{basePath}`: list my tenants, create, get record, and get logo.
15
+ - The stock event-doc CRUD at `{basePath}` ([defineEventDocRoutes](./event-doc-routes.md) for the `tenants` store, `tenant` type, with the publish sync wired in as `onPublish`, and `create` excluded — see below).
16
+ - The membership-gated routes at `{myTenantsBasePath}`: list my tenants, create, get record, and get logo.
17
17
 
18
18
  - **On AWS:** on the owner's deploy, this deploys everything [defineTenantStores](./tenant-stores.md) deploys (two DynamoDB tables and an S3 bucket) plus the API Gateway routes and Lambda handlers from [defineEventDocRoutes](./event-doc-routes.md) and the four tenant routes below. On every other service's deploy, only the `userTenantLinks` reference resolves (no new table); the inline functions deploy no infrastructure of their own anywhere.
19
19
 
@@ -26,6 +26,7 @@ export default [
26
26
  ...defineTenant({
27
27
  owner: { module: 'ca' },
28
28
  basePath: '/tenants',
29
+ myTenantsBasePath: '/my-tenants',
29
30
  routeAuthSettings: { userDirectoryName: 'users' },
30
31
  }),
31
32
  ];
@@ -37,12 +38,12 @@ All paths are prefixed with the version segment `/v{version}` (default `/v1`) an
37
38
 
38
39
  | Method | Path | Purpose |
39
40
  | --- | --- | --- |
40
- | `GET` | `{basePath}` | The authenticated user's tenants, as `EventDocSummary` rows. Runs under the request's scope: memberships homed in the caller's current partition hydrate live (drafts included); the rest hydrate from the published `TenantRecord` registry. |
41
- | `POST` | `{basePath}` | Create a tenant (body `{ name }`); the caller becomes its first member. Runs under the request's scope, so the new tenant doc lands in the caller's current partition. |
42
- | `GET` | `{basePath}/{id}` | One tenant's materialized record (the fast path). Members only: non-members get `Forbidden`, a missing record gets `NotFound`. |
43
- | `GET` | `{basePath}/{id}/logo` | A presigned, short-lived URL for the tenant's logo blob. Members only: non-members get `Forbidden`; a missing record or a tenant with no logo gets `NotFound`. Presigned in the scope the tenant doc was published under (recorded on the `TenantRecord`), not the reader's own scope, since the logo asset lives in the doc's home partition. |
41
+ | `GET` | `{myTenantsBasePath}` | The authenticated user's tenants, as `EventDocSummary` rows. Runs under the request's scope: memberships homed in the caller's current partition hydrate live (drafts included); the rest hydrate from the published `TenantRecord` registry. |
42
+ | `POST` | `{myTenantsBasePath}` | Create a tenant (body `{ name }`); the caller becomes its first member. Runs under the request's scope, so the new tenant doc lands in the caller's current partition. This is the only way to create a tenant — the stock `create` route is excluded at `{basePath}` so a new tenant is never made without also linking its creator. |
43
+ | `GET` | `{myTenantsBasePath}/{id}` | One tenant's materialized record (the fast path). Members only: non-members get `Forbidden`, a missing record gets `NotFound`. |
44
+ | `GET` | `{myTenantsBasePath}/{id}/logo` | A presigned, short-lived URL for the tenant's logo blob. Members only: non-members get `Forbidden`; a missing record or a tenant with no logo gets `NotFound`. Presigned in the scope the tenant doc was published under (recorded on the `TenantRecord`), not the reader's own scope, since the logo asset lives in the doc's home partition. |
44
45
 
45
- On top of these, the full generic event-doc route set (create, append, list, assets, and so on) is mounted under `{basePath}/docs`; see [defineEventDocRoutes](./event-doc-routes.md#routes-mounted) for the list.
46
+ On top of these, the stock event-doc route set (get, append, list events, assets, remove, and so on — everything but `create`) is mounted at `{basePath}`, named after the model type like any other collection; see [defineEventDocRoutes](./event-doc-routes.md#routes-mounted) for the list. `{basePath}` and `{myTenantsBasePath}` must be distinct and neither may be a path segment under the other: `{basePath}/{id}` matches any single segment, so a literal sibling path would be ambiguous with a tenant whose id happens to match it.
46
47
 
47
48
  ## Signature
48
49
 
@@ -57,7 +58,8 @@ The single `options` argument is a `TenantOptions` (a `TenantRoutesOptions` plus
57
58
  | Property | Type | Default | Description |
58
59
  | --- | --- | --- | --- |
59
60
  | `owner` | `CrossModuleOwner & { module: string }` | – (required) | The service that owns the tenant registry, e.g. `{ module: 'ca' }`. Pass the **same** value in every service's `defineTenant` call; the registry (stores, publish sync, management routes) only materializes when the deploying module matches this. |
60
- | `basePath` | `` `/${string}` `` | – (required) | URL prefix the tenant routes mount under, e.g. `/tenants`. The generic event-doc CRUD mounts under `{basePath}/docs`. Only used on the owner's deploy, but required on every call for type consistency across services. |
61
+ | `basePath` | `` `/${string}` `` | – (required) | The tenant collection root, e.g. `/tenants`. The stock event-doc CRUD (minus `create`) mounts here. Only used on the owner's deploy, but required on every call for type consistency across services. |
62
+ | `myTenantsBasePath` | `` `/${string}` `` | – (required) | URL prefix the membership-gated routes mount under, e.g. `/my-tenants` (list mine, create, get record, get logo). Must not be a child path of `basePath` — see [Routes mounted](#routes-mounted). Only used on the owner's deploy, but required on every call for type consistency across services. |
61
63
  | `routeAuthSettings` | `RouteAuthSettings` | – (required) | Auth applied to every mounted route (see [route](../webserver/route.md)). Required here, unlike the generic event-doc routes: tenant routes are meaningless unauthenticated, since membership keys off the user. Only used on the owner's deploy. |
62
64
  | `version` | `number` | `1` | Version number for the `/v{version}` path prefix on every route. Only used on the owner's deploy. |
63
65
  | `tenantHeaderName` | `string` | `'x-qpq-tenant-id'` | The header the client sends its selected tenant id on. Exposed to the tenant routes as the `tenantHeaderName` global, which the scope resolver reads. |
@@ -65,7 +67,7 @@ The single `options` argument is a `TenantOptions` (a `TenantRoutesOptions` plus
65
67
  ## Notes
66
68
 
67
69
  - Tenant state is event-sourced: the `tenants` event-doc collection is the audit-trailed source of truth, and the `tenantRecords` table is a read model synced on publish. Request handlers never write the record table directly.
68
- - The `tenants` collection is scope-resolved like any other tenanted collection (see [defineTenantedEventDoc](./tenanted-event-doc.md)): a doc is only visible/editable from the scope that owns it, including through the generic CRUD under `{basePath}/docs`. There is no cross-scope doc read — listing memberships homed in another scope goes through the published `TenantRecord`, not the doc store.
70
+ - The `tenants` collection is scope-resolved like any other tenanted collection (see [defineTenantedEventDoc](./tenanted-event-doc.md)): a doc is only visible/editable from the scope that owns it, including through the stock CRUD at `{basePath}`. There is no cross-scope doc read — listing memberships homed in another scope goes through the published `TenantRecord`, not the doc store.
69
71
  - The `TenantRecord` produced by the publish sync carries `tenantId`, `name`, `brandColors`, `logo` (an asset ref, resolved to a URL via the get-logo route above), `scope` (the storage scope the doc was published under, used to presign the logo for cross-scope readers), `createdAt`, `updatedAt`, `createdByUserId`, and a `status` derived from the summary (`deleted` when the summary has a `deletedAt`, otherwise `active`).
70
72
  - `defineTenant` registers the scope resolver but does not apply it to anything. To tenant-scope one of your own collections, pass `TENANT_SCOPE_RESOLVER_FN` as that collection's `scopeResolver` option (or use [defineTenantedEventDoc](./tenanted-event-doc.md), which does this for you).
71
73
  - The scope resolver always resolves to a typed scope — a membership-checked `TENANT#<id>` for a request that names a tenant, or the caller's own `PERSONAL#<userId>` when it doesn't. A tenant-scoped collection or connection is never left unscoped.
@@ -75,7 +77,7 @@ The single `options` argument is a `TenantOptions` (a `TenantRoutesOptions` plus
75
77
 
76
78
  - [defineTenantStores](./tenant-stores.md): the store half of this helper (owner-only).
77
79
  - [defineTenantedEventDoc](./tenanted-event-doc.md): a `defineEventDoc` with `TENANT_SCOPE_RESOLVER_FN` pre-wired as `scopeResolver`.
78
- - [defineEventDocRoutes](./event-doc-routes.md): the generic CRUD mounted under `{basePath}/docs`, and home of the `scopeResolver` / `onPublish` options.
80
+ - [defineEventDocRoutes](./event-doc-routes.md): the stock CRUD mounted at `{basePath}`, and home of the `scopeResolver` / `onPublish` / `excludeRoutes` options.
79
81
  - [defineWebSocketQueue](./web-socket-queue.md): where the tenant connection-scope resolver plugs in.
80
82
  - [defineTenantedWebSocketQueue](./tenanted-web-socket-queue.md): a `defineWebSocketQueue` with that resolver pre-wired.
81
83
  - [defineServiceSettings](../core/service-settings.md): the per-module gating mechanism this uses to materialize the registry only on the owner.