@derwinjs/db 0.10.0 → 0.11.0
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/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/milestone-event-store.d.ts +35 -0
- package/dist/milestone-event-store.d.ts.map +1 -0
- package/dist/milestone-event-store.js +221 -0
- package/dist/milestone-event-store.js.map +1 -0
- package/dist/rum-sample-store.d.ts +48 -0
- package/dist/rum-sample-store.d.ts.map +1 -0
- package/dist/rum-sample-store.js +155 -0
- package/dist/rum-sample-store.js.map +1 -0
- package/package.json +3 -3
- package/prisma/migrations/20260508000100_sprint11_phase3_rum_samples/migration.sql +31 -0
- package/prisma/migrations/20260508000200_sprint11_phase5_milestone_events/migration.sql +42 -0
- package/prisma/schema.prisma +82 -0
- package/prisma-client/edge.js +31 -4
- package/prisma-client/index-browser.js +28 -1
- package/prisma-client/index.d.ts +4102 -525
- package/prisma-client/index.js +31 -4
- package/prisma-client/package.json +1 -1
- package/prisma-client/schema.prisma +82 -0
- package/prisma-client/wasm.js +28 -1
package/dist/index.d.ts
CHANGED
|
@@ -36,5 +36,7 @@ export { createPrismaAutoPromotionEvaluator, DEFAULT_PROMOTION_THRESHOLDS, type
|
|
|
36
36
|
export { createPrismaVisualBaselineStore, type PrismaVisualBaselineStoreConfig, } from './visual-baseline-store.js';
|
|
37
37
|
export { createPrismaContractBaselineStore, type PrismaContractBaselineStoreConfig, } from './contract-baseline-store.js';
|
|
38
38
|
export { createPrismaTenantFuzzConfigStore, type PrismaTenantFuzzConfigStoreConfig, } from './tenant-fuzz-config-store.js';
|
|
39
|
+
export { createPrismaRumSampleStore, RumSampleStoreError, type PrismaRumSampleStoreConfig, } from './rum-sample-store.js';
|
|
40
|
+
export { createPrismaMilestoneEventStore, type PrismaMilestoneEventStoreConfig, } from './milestone-event-store.js';
|
|
39
41
|
export * from './prisma.js';
|
|
40
42
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,eAAO,MAAM,YAAY,EAAG,cAAuB,CAAC;AAIpD,OAAO,EAAE,yBAAyB,EAAE,KAAK,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AACjG,OAAO,EACL,6BAA6B,EAC7B,KAAK,6BAA6B,GACnC,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,sBAAsB,EAAE,KAAK,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AACxF,OAAO,EAAE,0BAA0B,EAAE,KAAK,0BAA0B,EAAE,MAAM,uBAAuB,CAAC;AACpG,OAAO,EAAE,yBAAyB,EAAE,KAAK,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AACjG,OAAO,EACL,6BAA6B,EAC7B,KAAK,6BAA6B,GACnC,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,iCAAiC,EACjC,KAAK,iCAAiC,GACvC,MAAM,8BAA8B,CAAC;AACtC,OAAO,EACL,kCAAkC,EAClC,KAAK,kCAAkC,GACxC,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,+BAA+B,EAC/B,KAAK,+BAA+B,GACrC,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,8BAA8B,EAC9B,KAAK,8BAA8B,GACpC,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,yBAAyB,EAAE,KAAK,yBAAyB,EAAE,MAAM,qBAAqB,CAAC;AAChG,OAAO,EACL,4BAA4B,EAC5B,SAAS,EACT,KAAK,4BAA4B,GAClC,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,uCAAuC,EACvC,KAAK,uCAAuC,GAC7C,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EACL,qCAAqC,EACrC,KAAK,qCAAqC,GAC3C,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EACL,iCAAiC,EACjC,SAAS,EACT,cAAc,EACd,KAAK,iCAAiC,GACvC,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,sBAAsB,EAAE,KAAK,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AACvF,OAAO,EACL,+BAA+B,EAC/B,8BAA8B,EAC9B,KAAK,+BAA+B,GACrC,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,4BAA4B,EAC5B,KAAK,4BAA4B,GAClC,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,kCAAkC,EAClC,4BAA4B,EAC5B,KAAK,kCAAkC,GACxC,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,+BAA+B,EAC/B,KAAK,+BAA+B,GACrC,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,iCAAiC,EACjC,KAAK,iCAAiC,GACvC,MAAM,8BAA8B,CAAC;AACtC,OAAO,EACL,iCAAiC,EACjC,KAAK,iCAAiC,GACvC,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,eAAO,MAAM,YAAY,EAAG,cAAuB,CAAC;AAIpD,OAAO,EAAE,yBAAyB,EAAE,KAAK,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AACjG,OAAO,EACL,6BAA6B,EAC7B,KAAK,6BAA6B,GACnC,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,sBAAsB,EAAE,KAAK,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AACxF,OAAO,EAAE,0BAA0B,EAAE,KAAK,0BAA0B,EAAE,MAAM,uBAAuB,CAAC;AACpG,OAAO,EAAE,yBAAyB,EAAE,KAAK,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AACjG,OAAO,EACL,6BAA6B,EAC7B,KAAK,6BAA6B,GACnC,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,iCAAiC,EACjC,KAAK,iCAAiC,GACvC,MAAM,8BAA8B,CAAC;AACtC,OAAO,EACL,kCAAkC,EAClC,KAAK,kCAAkC,GACxC,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,+BAA+B,EAC/B,KAAK,+BAA+B,GACrC,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,8BAA8B,EAC9B,KAAK,8BAA8B,GACpC,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,yBAAyB,EAAE,KAAK,yBAAyB,EAAE,MAAM,qBAAqB,CAAC;AAChG,OAAO,EACL,4BAA4B,EAC5B,SAAS,EACT,KAAK,4BAA4B,GAClC,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,uCAAuC,EACvC,KAAK,uCAAuC,GAC7C,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EACL,qCAAqC,EACrC,KAAK,qCAAqC,GAC3C,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EACL,iCAAiC,EACjC,SAAS,EACT,cAAc,EACd,KAAK,iCAAiC,GACvC,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,sBAAsB,EAAE,KAAK,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AACvF,OAAO,EACL,+BAA+B,EAC/B,8BAA8B,EAC9B,KAAK,+BAA+B,GACrC,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,4BAA4B,EAC5B,KAAK,4BAA4B,GAClC,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,kCAAkC,EAClC,4BAA4B,EAC5B,KAAK,kCAAkC,GACxC,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,+BAA+B,EAC/B,KAAK,+BAA+B,GACrC,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,iCAAiC,EACjC,KAAK,iCAAiC,GACvC,MAAM,8BAA8B,CAAC;AACtC,OAAO,EACL,iCAAiC,EACjC,KAAK,iCAAiC,GACvC,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,0BAA0B,EAC1B,mBAAmB,EACnB,KAAK,0BAA0B,GAChC,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,+BAA+B,EAC/B,KAAK,+BAA+B,GACrC,MAAM,4BAA4B,CAAC;AA4BpC,cAAc,aAAa,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -37,6 +37,8 @@ export { createPrismaAutoPromotionEvaluator, DEFAULT_PROMOTION_THRESHOLDS, } fro
|
|
|
37
37
|
export { createPrismaVisualBaselineStore, } from './visual-baseline-store.js';
|
|
38
38
|
export { createPrismaContractBaselineStore, } from './contract-baseline-store.js';
|
|
39
39
|
export { createPrismaTenantFuzzConfigStore, } from './tenant-fuzz-config-store.js';
|
|
40
|
+
export { createPrismaRumSampleStore, RumSampleStoreError, } from './rum-sample-store.js';
|
|
41
|
+
export { createPrismaMilestoneEventStore, } from './milestone-event-store.js';
|
|
40
42
|
// ─── Prisma client re-export ─────────────────────────────────────────────
|
|
41
43
|
//
|
|
42
44
|
// @derwinjs/db ships its OWN generated Prisma client (output: prisma-client/
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,MAAM,CAAC,MAAM,YAAY,GAAG,cAAuB,CAAC;AAEpD,4EAA4E;AAE5E,OAAO,EAAE,yBAAyB,EAAkC,MAAM,sBAAsB,CAAC;AACjG,OAAO,EACL,6BAA6B,GAE9B,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,sBAAsB,EAA+B,MAAM,mBAAmB,CAAC;AACxF,OAAO,EAAE,0BAA0B,EAAmC,MAAM,uBAAuB,CAAC;AACpG,OAAO,EAAE,yBAAyB,EAAkC,MAAM,sBAAsB,CAAC;AACjG,OAAO,EACL,6BAA6B,GAE9B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,iCAAiC,GAElC,MAAM,8BAA8B,CAAC;AACtC,OAAO,EACL,kCAAkC,GAEnC,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,+BAA+B,GAEhC,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,8BAA8B,GAE/B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,yBAAyB,EAAkC,MAAM,qBAAqB,CAAC;AAChG,OAAO,EACL,4BAA4B,EAC5B,SAAS,GAEV,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,uCAAuC,GAExC,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EACL,qCAAqC,GAEtC,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EACL,iCAAiC,EACjC,SAAS,EACT,cAAc,GAEf,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,sBAAsB,EAA+B,MAAM,kBAAkB,CAAC;AACvF,OAAO,EACL,+BAA+B,EAC/B,8BAA8B,GAE/B,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,4BAA4B,GAE7B,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,kCAAkC,EAClC,4BAA4B,GAE7B,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,+BAA+B,GAEhC,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,iCAAiC,GAElC,MAAM,8BAA8B,CAAC;AACtC,OAAO,EACL,iCAAiC,GAElC,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,MAAM,CAAC,MAAM,YAAY,GAAG,cAAuB,CAAC;AAEpD,4EAA4E;AAE5E,OAAO,EAAE,yBAAyB,EAAkC,MAAM,sBAAsB,CAAC;AACjG,OAAO,EACL,6BAA6B,GAE9B,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,sBAAsB,EAA+B,MAAM,mBAAmB,CAAC;AACxF,OAAO,EAAE,0BAA0B,EAAmC,MAAM,uBAAuB,CAAC;AACpG,OAAO,EAAE,yBAAyB,EAAkC,MAAM,sBAAsB,CAAC;AACjG,OAAO,EACL,6BAA6B,GAE9B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,iCAAiC,GAElC,MAAM,8BAA8B,CAAC;AACtC,OAAO,EACL,kCAAkC,GAEnC,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,+BAA+B,GAEhC,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,8BAA8B,GAE/B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,yBAAyB,EAAkC,MAAM,qBAAqB,CAAC;AAChG,OAAO,EACL,4BAA4B,EAC5B,SAAS,GAEV,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,uCAAuC,GAExC,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EACL,qCAAqC,GAEtC,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EACL,iCAAiC,EACjC,SAAS,EACT,cAAc,GAEf,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,sBAAsB,EAA+B,MAAM,kBAAkB,CAAC;AACvF,OAAO,EACL,+BAA+B,EAC/B,8BAA8B,GAE/B,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,4BAA4B,GAE7B,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,kCAAkC,EAClC,4BAA4B,GAE7B,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,+BAA+B,GAEhC,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,iCAAiC,GAElC,MAAM,8BAA8B,CAAC;AACtC,OAAO,EACL,iCAAiC,GAElC,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,0BAA0B,EAC1B,mBAAmB,GAEpB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,+BAA+B,GAEhC,MAAM,4BAA4B,CAAC;AAEpC,4EAA4E;AAC5E,EAAE;AACF,6EAA6E;AAC7E,wEAAwE;AACxE,sEAAsE;AACtE,wEAAwE;AACxE,wEAAwE;AACxE,gDAAgD;AAChD,EAAE;AACF,+DAA+D;AAC/D,0EAA0E;AAC1E,yCAAyC;AACzC,+EAA+E;AAC/E,yCAAyC;AACzC,uEAAuE;AACvE,sEAAsE;AACtE,kEAAkE;AAClE,+BAA+B;AAC/B,EAAE;AACF,oBAAoB;AACpB,8DAA8D;AAC9D,4CAA4C;AAC5C,qEAAqE;AACrE,QAAQ;AACR,gDAAgD;AAEhD,cAAc,aAAa,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createPrismaMilestoneEventStore — Prisma-backed implementation of the SDK
|
|
3
|
+
* MilestoneEventStore contract introduced by QAP-115.
|
|
4
|
+
*
|
|
5
|
+
* Sprint 11 Phase 5 (Cross-product tracking timeline). One row per
|
|
6
|
+
* milestone in the `milestone_events` table, project-scoped via FK. v1
|
|
7
|
+
* is read-mostly (CRUD); drag-to-reschedule deferred to Sprint 14.
|
|
8
|
+
*
|
|
9
|
+
* Tenant isolation: every method scopes by projectId. Pattern D applies —
|
|
10
|
+
* `update` / `delete` against a row owned by a different projectId
|
|
11
|
+
* surfaces as `not_found` rather than `tenant_mismatch` (defense-in-depth
|
|
12
|
+
* against tenant enumeration). The composite (id, projectId) unique-ish
|
|
13
|
+
* lookup on `update` / `delete` is what enforces this — Prisma's
|
|
14
|
+
* `update({ where: { id, projectId } })` raises P2025 when nothing
|
|
15
|
+
* matches, and `deleteMany({ where: { id, projectId } })` returns
|
|
16
|
+
* `count: 0`.
|
|
17
|
+
*
|
|
18
|
+
* Validation runs at the factory boundary BEFORE any Prisma call — empty
|
|
19
|
+
* `name` / `kind` / `createdBy` / `projectId` throw `invalid_input` so
|
|
20
|
+
* the error surfaces with a clear caller-bug code rather than as a
|
|
21
|
+
* Prisma constraint violation.
|
|
22
|
+
*
|
|
23
|
+
* The kind column is intentionally a free-form String (not a Prisma
|
|
24
|
+
* enum). The factory enforces non-empty + trims; consumers may emit
|
|
25
|
+
* custom kinds (the conflict-detection helpers in @derwinjs/core treat
|
|
26
|
+
* unknown kinds as "no conflict computed against this kind").
|
|
27
|
+
*/
|
|
28
|
+
import { type PrismaClient } from './prisma.js';
|
|
29
|
+
import { type MilestoneEventStore } from '@derwinjs/sdk';
|
|
30
|
+
export interface PrismaMilestoneEventStoreConfig {
|
|
31
|
+
/** Generated Prisma client. Pass an instance per process. */
|
|
32
|
+
prisma: PrismaClient;
|
|
33
|
+
}
|
|
34
|
+
export declare function createPrismaMilestoneEventStore(config: PrismaMilestoneEventStoreConfig): MilestoneEventStore;
|
|
35
|
+
//# sourceMappingURL=milestone-event-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"milestone-event-store.d.ts","sourceRoot":"","sources":["../src/milestone-event-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAU,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAKL,KAAK,mBAAmB,EAEzB,MAAM,eAAe,CAAC;AAIvB,MAAM,WAAW,+BAA+B;IAC9C,6DAA6D;IAC7D,MAAM,EAAE,YAAY,CAAC;CACtB;AA6ED,wBAAgB,+BAA+B,CAC7C,MAAM,EAAE,+BAA+B,GACtC,mBAAmB,CAqJrB"}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createPrismaMilestoneEventStore — Prisma-backed implementation of the SDK
|
|
3
|
+
* MilestoneEventStore contract introduced by QAP-115.
|
|
4
|
+
*
|
|
5
|
+
* Sprint 11 Phase 5 (Cross-product tracking timeline). One row per
|
|
6
|
+
* milestone in the `milestone_events` table, project-scoped via FK. v1
|
|
7
|
+
* is read-mostly (CRUD); drag-to-reschedule deferred to Sprint 14.
|
|
8
|
+
*
|
|
9
|
+
* Tenant isolation: every method scopes by projectId. Pattern D applies —
|
|
10
|
+
* `update` / `delete` against a row owned by a different projectId
|
|
11
|
+
* surfaces as `not_found` rather than `tenant_mismatch` (defense-in-depth
|
|
12
|
+
* against tenant enumeration). The composite (id, projectId) unique-ish
|
|
13
|
+
* lookup on `update` / `delete` is what enforces this — Prisma's
|
|
14
|
+
* `update({ where: { id, projectId } })` raises P2025 when nothing
|
|
15
|
+
* matches, and `deleteMany({ where: { id, projectId } })` returns
|
|
16
|
+
* `count: 0`.
|
|
17
|
+
*
|
|
18
|
+
* Validation runs at the factory boundary BEFORE any Prisma call — empty
|
|
19
|
+
* `name` / `kind` / `createdBy` / `projectId` throw `invalid_input` so
|
|
20
|
+
* the error surfaces with a clear caller-bug code rather than as a
|
|
21
|
+
* Prisma constraint violation.
|
|
22
|
+
*
|
|
23
|
+
* The kind column is intentionally a free-form String (not a Prisma
|
|
24
|
+
* enum). The factory enforces non-empty + trims; consumers may emit
|
|
25
|
+
* custom kinds (the conflict-detection helpers in @derwinjs/core treat
|
|
26
|
+
* unknown kinds as "no conflict computed against this kind").
|
|
27
|
+
*/
|
|
28
|
+
import { Prisma } from './prisma.js';
|
|
29
|
+
import { MilestoneEventStoreError, } from '@derwinjs/sdk';
|
|
30
|
+
// ─── Validation ──────────────────────────────────────────────────────────
|
|
31
|
+
function assertNonEmpty(value, fieldName) {
|
|
32
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
33
|
+
throw new MilestoneEventStoreError('invalid_input', `createPrismaMilestoneEventStore: ${fieldName} is required and non-empty`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function assertDate(value, fieldName) {
|
|
37
|
+
if (!(value instanceof Date) || Number.isNaN(value.getTime())) {
|
|
38
|
+
throw new MilestoneEventStoreError('invalid_input', `createPrismaMilestoneEventStore: ${fieldName} must be a valid Date`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function assertCreateInput(input) {
|
|
42
|
+
assertNonEmpty(input.projectId, 'projectId');
|
|
43
|
+
assertNonEmpty(input.name, 'name');
|
|
44
|
+
assertNonEmpty(input.kind, 'kind');
|
|
45
|
+
assertNonEmpty(input.createdBy, 'createdBy');
|
|
46
|
+
assertDate(input.startsAt, 'startsAt');
|
|
47
|
+
if (input.endsAt !== undefined) {
|
|
48
|
+
assertDate(input.endsAt, 'endsAt');
|
|
49
|
+
if (input.endsAt.getTime() < input.startsAt.getTime()) {
|
|
50
|
+
throw new MilestoneEventStoreError('invalid_input', 'createPrismaMilestoneEventStore: endsAt must be greater than or equal to startsAt');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (input.description !== undefined && typeof input.description !== 'string') {
|
|
54
|
+
throw new MilestoneEventStoreError('invalid_input', 'createPrismaMilestoneEventStore: description must be a string when present');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function assertUpdatePatch(patch) {
|
|
58
|
+
if (patch.name !== undefined)
|
|
59
|
+
assertNonEmpty(patch.name, 'name');
|
|
60
|
+
if (patch.kind !== undefined)
|
|
61
|
+
assertNonEmpty(patch.kind, 'kind');
|
|
62
|
+
if (patch.startsAt !== undefined)
|
|
63
|
+
assertDate(patch.startsAt, 'startsAt');
|
|
64
|
+
if (patch.endsAt !== undefined && patch.endsAt !== null)
|
|
65
|
+
assertDate(patch.endsAt, 'endsAt');
|
|
66
|
+
if (patch.description !== undefined &&
|
|
67
|
+
patch.description !== null &&
|
|
68
|
+
typeof patch.description !== 'string') {
|
|
69
|
+
throw new MilestoneEventStoreError('invalid_input', 'createPrismaMilestoneEventStore: description must be a string or null when present');
|
|
70
|
+
}
|
|
71
|
+
// Cross-field check: if BOTH startsAt and endsAt are in the patch and
|
|
72
|
+
// both non-null, they must satisfy startsAt <= endsAt.
|
|
73
|
+
if (patch.startsAt !== undefined &&
|
|
74
|
+
patch.endsAt !== undefined &&
|
|
75
|
+
patch.endsAt !== null &&
|
|
76
|
+
patch.endsAt.getTime() < patch.startsAt.getTime()) {
|
|
77
|
+
throw new MilestoneEventStoreError('invalid_input', 'createPrismaMilestoneEventStore: endsAt must be greater than or equal to startsAt');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// ─── Factory ─────────────────────────────────────────────────────────────
|
|
81
|
+
export function createPrismaMilestoneEventStore(config) {
|
|
82
|
+
const { prisma } = config;
|
|
83
|
+
return {
|
|
84
|
+
async list(filter) {
|
|
85
|
+
assertNonEmpty(filter.projectId, 'projectId');
|
|
86
|
+
const where = { projectId: filter.projectId };
|
|
87
|
+
const now = Date.now();
|
|
88
|
+
if (typeof filter.sinceMs === 'number' && Number.isFinite(filter.sinceMs)) {
|
|
89
|
+
const since = new Date(now - filter.sinceMs);
|
|
90
|
+
// A range milestone is "in window" if its startsAt OR endsAt
|
|
91
|
+
// falls in the trailing window. Point-in-time milestones (no
|
|
92
|
+
// endsAt) match on startsAt only.
|
|
93
|
+
where.OR = [{ startsAt: { gte: since } }, { endsAt: { gte: since } }];
|
|
94
|
+
}
|
|
95
|
+
if (typeof filter.untilMs === 'number' && Number.isFinite(filter.untilMs)) {
|
|
96
|
+
const until = new Date(now + filter.untilMs);
|
|
97
|
+
where.startsAt = { ...where.startsAt, lte: until };
|
|
98
|
+
}
|
|
99
|
+
if (Array.isArray(filter.kinds) && filter.kinds.length > 0) {
|
|
100
|
+
where.kind = { in: filter.kinds };
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const rows = await prisma.milestoneEvent.findMany({
|
|
104
|
+
where,
|
|
105
|
+
orderBy: { startsAt: 'asc' },
|
|
106
|
+
});
|
|
107
|
+
return rows.map(toMilestoneEvent);
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
throw new MilestoneEventStoreError('storage_failed', `createPrismaMilestoneEventStore: list raised: ${err.message}`, err);
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
async create(input) {
|
|
114
|
+
assertCreateInput(input);
|
|
115
|
+
try {
|
|
116
|
+
const row = await prisma.milestoneEvent.create({
|
|
117
|
+
data: {
|
|
118
|
+
projectId: input.projectId,
|
|
119
|
+
name: input.name,
|
|
120
|
+
description: input.description ?? null,
|
|
121
|
+
startsAt: input.startsAt,
|
|
122
|
+
endsAt: input.endsAt ?? null,
|
|
123
|
+
kind: input.kind,
|
|
124
|
+
createdBy: input.createdBy,
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
return toMilestoneEvent(row);
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === 'P2003') {
|
|
131
|
+
// FK violation on projectId — surface as invalid_input rather
|
|
132
|
+
// than storage_failed so the API layer can return 400 (the
|
|
133
|
+
// operator passed a nonexistent projectId, not a transient
|
|
134
|
+
// storage problem).
|
|
135
|
+
throw new MilestoneEventStoreError('invalid_input', `createPrismaMilestoneEventStore: projectId ${input.projectId} does not resolve to a Project row`, err);
|
|
136
|
+
}
|
|
137
|
+
throw new MilestoneEventStoreError('storage_failed', `createPrismaMilestoneEventStore: create raised: ${err.message}`, err);
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
async update(id, projectId, patch) {
|
|
141
|
+
assertNonEmpty(id, 'id');
|
|
142
|
+
assertNonEmpty(projectId, 'projectId');
|
|
143
|
+
assertUpdatePatch(patch);
|
|
144
|
+
// Two-step: updateMany returns count so we can distinguish
|
|
145
|
+
// not-found / wrong-tenant (count === 0) from a successful update
|
|
146
|
+
// without leaking existence via the standard `update` API's P2025.
|
|
147
|
+
try {
|
|
148
|
+
const data = {};
|
|
149
|
+
if (patch.name !== undefined)
|
|
150
|
+
data.name = patch.name;
|
|
151
|
+
if (patch.description !== undefined)
|
|
152
|
+
data.description = patch.description;
|
|
153
|
+
if (patch.startsAt !== undefined)
|
|
154
|
+
data.startsAt = patch.startsAt;
|
|
155
|
+
if (patch.endsAt !== undefined)
|
|
156
|
+
data.endsAt = patch.endsAt;
|
|
157
|
+
if (patch.kind !== undefined)
|
|
158
|
+
data.kind = patch.kind;
|
|
159
|
+
const result = await prisma.milestoneEvent.updateMany({
|
|
160
|
+
where: { id, projectId },
|
|
161
|
+
data,
|
|
162
|
+
});
|
|
163
|
+
if (result.count === 0) {
|
|
164
|
+
throw new MilestoneEventStoreError('not_found', `createPrismaMilestoneEventStore: milestone id=${id} not found in project ${projectId}`);
|
|
165
|
+
}
|
|
166
|
+
// Re-read to return the fresh row. updatedAt has been refreshed
|
|
167
|
+
// by Prisma's @updatedAt directive at this point.
|
|
168
|
+
const fresh = await prisma.milestoneEvent.findFirst({
|
|
169
|
+
where: { id, projectId },
|
|
170
|
+
});
|
|
171
|
+
if (fresh === null) {
|
|
172
|
+
// Concurrent delete between updateMany + findFirst. Rare; surface
|
|
173
|
+
// as not_found.
|
|
174
|
+
throw new MilestoneEventStoreError('not_found', `createPrismaMilestoneEventStore: milestone id=${id} disappeared after update in project ${projectId}`);
|
|
175
|
+
}
|
|
176
|
+
return toMilestoneEvent(fresh);
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
if (err instanceof MilestoneEventStoreError)
|
|
180
|
+
throw err;
|
|
181
|
+
throw new MilestoneEventStoreError('storage_failed', `createPrismaMilestoneEventStore: update raised: ${err.message}`, err);
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
async delete(id, projectId) {
|
|
185
|
+
assertNonEmpty(id, 'id');
|
|
186
|
+
assertNonEmpty(projectId, 'projectId');
|
|
187
|
+
try {
|
|
188
|
+
const result = await prisma.milestoneEvent.deleteMany({
|
|
189
|
+
where: { id, projectId },
|
|
190
|
+
});
|
|
191
|
+
return result.count > 0;
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
throw new MilestoneEventStoreError('storage_failed', `createPrismaMilestoneEventStore: delete raised: ${err.message}`, err);
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Project a Prisma row to the SDK MilestoneEvent shape. Per memory
|
|
201
|
+
* rule 22, no defensive `as PrismaMilestoneEventRow` cast at the
|
|
202
|
+
* argument site — the inferred Prisma return type is exact after
|
|
203
|
+
* `prisma generate`. The inline interface above just documents the
|
|
204
|
+
* shape for readers; it's structurally compatible with the generated
|
|
205
|
+
* MilestoneEventGetPayload.
|
|
206
|
+
*/
|
|
207
|
+
function toMilestoneEvent(row) {
|
|
208
|
+
return {
|
|
209
|
+
id: row.id,
|
|
210
|
+
projectId: row.projectId,
|
|
211
|
+
name: row.name,
|
|
212
|
+
description: row.description,
|
|
213
|
+
startsAt: row.startsAt,
|
|
214
|
+
endsAt: row.endsAt,
|
|
215
|
+
kind: row.kind,
|
|
216
|
+
createdBy: row.createdBy,
|
|
217
|
+
createdAt: row.createdAt,
|
|
218
|
+
updatedAt: row.updatedAt,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
//# sourceMappingURL=milestone-event-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"milestone-event-store.js","sourceRoot":"","sources":["../src/milestone-event-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createPrismaRumSampleStore — Prisma-backed implementation of the SDK
|
|
3
|
+
* RumSampleStore contract introduced by QAP-113.
|
|
4
|
+
*
|
|
5
|
+
* Sprint 11 Phase 3 (Test Surfaces — RUM ingestion). Samples are stored as
|
|
6
|
+
* one row per insert against the `RumSample` model. There is no uniqueness
|
|
7
|
+
* constraint on (projectId, metric) — a single page-load may emit multiple
|
|
8
|
+
* samples per metric, and operators need the full distribution to derive
|
|
9
|
+
* percentiles.
|
|
10
|
+
*
|
|
11
|
+
* Tenant isolation: every method scopes by projectId. Pattern D applies —
|
|
12
|
+
* unknown / wrong-project lookups return null from `getBaseline` rather
|
|
13
|
+
* than throwing or leaking existence (defense-in-depth against tenant
|
|
14
|
+
* enumeration). The "no samples in window" return is also null, so
|
|
15
|
+
* cross-tenant probes look identical to "no data this window".
|
|
16
|
+
*
|
|
17
|
+
* Percentile math: derived in-memory via array-index math after the
|
|
18
|
+
* `findMany` returns a value-sorted result set. No third-party stats
|
|
19
|
+
* library — RumSample row sets per (projectId, metric, window) cap at the
|
|
20
|
+
* tens-of-thousands order of magnitude, which fits comfortably in process
|
|
21
|
+
* memory for the percentile read.
|
|
22
|
+
*/
|
|
23
|
+
import type { PrismaClient } from './prisma.js';
|
|
24
|
+
import type { RumSampleStore } from '@derwinjs/sdk';
|
|
25
|
+
export interface PrismaRumSampleStoreConfig {
|
|
26
|
+
/** Generated Prisma client. Pass an instance per process. */
|
|
27
|
+
prisma: PrismaClient;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Errors thrown by the Prisma-backed RumSampleStore. The store-level
|
|
31
|
+
* errors are kept inside @derwinjs/db (NOT promoted to @derwinjs/sdk)
|
|
32
|
+
* because they reflect storage-layer specifics; the route layer maps
|
|
33
|
+
* them to the operator-facing WebVitalsIngestorError before crossing
|
|
34
|
+
* the SDK boundary.
|
|
35
|
+
*
|
|
36
|
+
* The discriminating `code` field lets callers route handling:
|
|
37
|
+
* - `invalid_input` — caller bug rejected at the factory boundary; do
|
|
38
|
+
* not retry.
|
|
39
|
+
* - `io_failed` — underlying Prisma call raised; transient — retry
|
|
40
|
+
* or escalate.
|
|
41
|
+
*/
|
|
42
|
+
export declare class RumSampleStoreError extends Error {
|
|
43
|
+
readonly code: 'invalid_input' | 'io_failed';
|
|
44
|
+
readonly cause?: unknown | undefined;
|
|
45
|
+
constructor(code: 'invalid_input' | 'io_failed', message: string, cause?: unknown | undefined);
|
|
46
|
+
}
|
|
47
|
+
export declare function createPrismaRumSampleStore(config: PrismaRumSampleStoreConfig): RumSampleStore;
|
|
48
|
+
//# sourceMappingURL=rum-sample-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rum-sample-store.d.ts","sourceRoot":"","sources":["../src/rum-sample-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAe,cAAc,EAAE,MAAM,eAAe,CAAC;AAIjE,MAAM,WAAW,0BAA0B;IACzC,6DAA6D;IAC7D,MAAM,EAAE,YAAY,CAAC;CACtB;AAID;;;;;;;;;;;;GAYG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;aAE1B,IAAI,EAAE,eAAe,GAAG,WAAW;aAE1B,KAAK,CAAC,EAAE,OAAO;gBAFxB,IAAI,EAAE,eAAe,GAAG,WAAW,EACnD,OAAO,EAAE,MAAM,EACU,KAAK,CAAC,EAAE,OAAO,YAAA;CAM3C;AAmED,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,0BAA0B,GAAG,cAAc,CAwF7F"}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createPrismaRumSampleStore — Prisma-backed implementation of the SDK
|
|
3
|
+
* RumSampleStore contract introduced by QAP-113.
|
|
4
|
+
*
|
|
5
|
+
* Sprint 11 Phase 3 (Test Surfaces — RUM ingestion). Samples are stored as
|
|
6
|
+
* one row per insert against the `RumSample` model. There is no uniqueness
|
|
7
|
+
* constraint on (projectId, metric) — a single page-load may emit multiple
|
|
8
|
+
* samples per metric, and operators need the full distribution to derive
|
|
9
|
+
* percentiles.
|
|
10
|
+
*
|
|
11
|
+
* Tenant isolation: every method scopes by projectId. Pattern D applies —
|
|
12
|
+
* unknown / wrong-project lookups return null from `getBaseline` rather
|
|
13
|
+
* than throwing or leaking existence (defense-in-depth against tenant
|
|
14
|
+
* enumeration). The "no samples in window" return is also null, so
|
|
15
|
+
* cross-tenant probes look identical to "no data this window".
|
|
16
|
+
*
|
|
17
|
+
* Percentile math: derived in-memory via array-index math after the
|
|
18
|
+
* `findMany` returns a value-sorted result set. No third-party stats
|
|
19
|
+
* library — RumSample row sets per (projectId, metric, window) cap at the
|
|
20
|
+
* tens-of-thousands order of magnitude, which fits comfortably in process
|
|
21
|
+
* memory for the percentile read.
|
|
22
|
+
*/
|
|
23
|
+
// ─── Error class ─────────────────────────────────────────────────────────
|
|
24
|
+
/**
|
|
25
|
+
* Errors thrown by the Prisma-backed RumSampleStore. The store-level
|
|
26
|
+
* errors are kept inside @derwinjs/db (NOT promoted to @derwinjs/sdk)
|
|
27
|
+
* because they reflect storage-layer specifics; the route layer maps
|
|
28
|
+
* them to the operator-facing WebVitalsIngestorError before crossing
|
|
29
|
+
* the SDK boundary.
|
|
30
|
+
*
|
|
31
|
+
* The discriminating `code` field lets callers route handling:
|
|
32
|
+
* - `invalid_input` — caller bug rejected at the factory boundary; do
|
|
33
|
+
* not retry.
|
|
34
|
+
* - `io_failed` — underlying Prisma call raised; transient — retry
|
|
35
|
+
* or escalate.
|
|
36
|
+
*/
|
|
37
|
+
export class RumSampleStoreError extends Error {
|
|
38
|
+
code;
|
|
39
|
+
cause;
|
|
40
|
+
constructor(code, message, cause) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.code = code;
|
|
43
|
+
this.cause = cause;
|
|
44
|
+
this.name = 'RumSampleStoreError';
|
|
45
|
+
Object.setPrototypeOf(this, RumSampleStoreError.prototype);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// ─── Validation ──────────────────────────────────────────────────────────
|
|
49
|
+
function assertNonEmpty(value, fieldName) {
|
|
50
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
51
|
+
throw new RumSampleStoreError('invalid_input', `createPrismaRumSampleStore: ${fieldName} is required and non-empty`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function assertFiniteNumber(value, fieldName) {
|
|
55
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
56
|
+
throw new RumSampleStoreError('invalid_input', `createPrismaRumSampleStore: ${fieldName} must be a finite number`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function assertNonNegativeMs(value, fieldName) {
|
|
60
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
|
|
61
|
+
throw new RumSampleStoreError('invalid_input', `createPrismaRumSampleStore: ${fieldName} must be a non-negative finite number`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// ─── Percentile helper ──────────────────────────────────────────────────
|
|
65
|
+
/**
|
|
66
|
+
* Nearest-rank percentile over a value-sorted ascending array. Mirrors the
|
|
67
|
+
* convention web-vitals.js + most RUM analytics tools use:
|
|
68
|
+
*
|
|
69
|
+
* index = ceil((p / 100) * N) - 1, clamped to [0, N-1].
|
|
70
|
+
*
|
|
71
|
+
* For a single-element array all percentiles return that element. For
|
|
72
|
+
* pre-sorted ascending arrays this is the cheapest correct percentile; the
|
|
73
|
+
* caller is responsible for sorting before passing the array in.
|
|
74
|
+
*/
|
|
75
|
+
function percentile(sortedAsc, p) {
|
|
76
|
+
const n = sortedAsc.length;
|
|
77
|
+
if (n === 0) {
|
|
78
|
+
throw new RumSampleStoreError('invalid_input', 'createPrismaRumSampleStore: percentile() called on empty array (caller bug)');
|
|
79
|
+
}
|
|
80
|
+
const idx = Math.max(0, Math.min(n - 1, Math.ceil((p / 100) * n) - 1));
|
|
81
|
+
// idx is in [0, n-1] and n >= 1, so the slot is always set. Runtime-guard
|
|
82
|
+
// form (memory rule 13) instead of `!` or `as number` — both are banned by
|
|
83
|
+
// @typescript-eslint/no-non-null-assertion + non-nullable-type-assertion-style.
|
|
84
|
+
const value = sortedAsc[idx];
|
|
85
|
+
if (value === undefined) {
|
|
86
|
+
throw new RumSampleStoreError('invalid_input', `createPrismaRumSampleStore: percentile() unreachable branch — idx=${String(idx)} of ${String(n)}`);
|
|
87
|
+
}
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
// ─── Factory ─────────────────────────────────────────────────────────────
|
|
91
|
+
export function createPrismaRumSampleStore(config) {
|
|
92
|
+
const { prisma } = config;
|
|
93
|
+
return {
|
|
94
|
+
async insert(input) {
|
|
95
|
+
assertNonEmpty(input.projectId, 'projectId');
|
|
96
|
+
assertNonEmpty(input.metric, 'metric');
|
|
97
|
+
assertFiniteNumber(input.value, 'value');
|
|
98
|
+
assertNonEmpty(input.rating, 'rating');
|
|
99
|
+
assertNonEmpty(input.url, 'url');
|
|
100
|
+
try {
|
|
101
|
+
const row = await prisma.rumSample.create({
|
|
102
|
+
data: {
|
|
103
|
+
projectId: input.projectId,
|
|
104
|
+
metric: input.metric,
|
|
105
|
+
value: input.value,
|
|
106
|
+
rating: input.rating,
|
|
107
|
+
url: input.url,
|
|
108
|
+
deviceType: input.deviceType ?? null,
|
|
109
|
+
sessionId: input.sessionId ?? null,
|
|
110
|
+
},
|
|
111
|
+
select: { id: true },
|
|
112
|
+
});
|
|
113
|
+
return { id: row.id };
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
throw new RumSampleStoreError('io_failed', `createPrismaRumSampleStore: insert raised: ${err.message}`, err);
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
async getBaseline(input) {
|
|
120
|
+
assertNonEmpty(input.projectId, 'projectId');
|
|
121
|
+
assertNonEmpty(input.metric, 'metric');
|
|
122
|
+
assertNonNegativeMs(input.sinceMs, 'sinceMs');
|
|
123
|
+
const windowEnd = new Date();
|
|
124
|
+
const windowStart = new Date(windowEnd.getTime() - input.sinceMs);
|
|
125
|
+
let rows;
|
|
126
|
+
try {
|
|
127
|
+
rows = await prisma.rumSample.findMany({
|
|
128
|
+
where: {
|
|
129
|
+
projectId: input.projectId,
|
|
130
|
+
metric: input.metric,
|
|
131
|
+
capturedAt: { gte: windowStart },
|
|
132
|
+
},
|
|
133
|
+
orderBy: { value: 'asc' },
|
|
134
|
+
select: { value: true },
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
throw new RumSampleStoreError('io_failed', `createPrismaRumSampleStore: getBaseline raised: ${err.message}`, err);
|
|
139
|
+
}
|
|
140
|
+
if (rows.length === 0)
|
|
141
|
+
return null;
|
|
142
|
+
const values = rows.map((r) => r.value);
|
|
143
|
+
return {
|
|
144
|
+
metric: input.metric,
|
|
145
|
+
p50: percentile(values, 50),
|
|
146
|
+
p75: percentile(values, 75),
|
|
147
|
+
p95: percentile(values, 95),
|
|
148
|
+
sampleCount: values.length,
|
|
149
|
+
windowStart,
|
|
150
|
+
windowEnd,
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=rum-sample-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rum-sample-store.js","sourceRoot":"","sources":["../src/rum-sample-store.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@derwinjs/db",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Prisma schema + migrations for Derwin's own Postgres. 14 models, project-namespaced. Per ADR-0005. Ships its own generated Prisma client (multi-platform binaries) for cross-consumer compatibility.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"type": "module",
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@prisma/client": "^5.22.0",
|
|
36
|
-
"@derwinjs/
|
|
37
|
-
"@derwinjs/
|
|
36
|
+
"@derwinjs/core": "0.11.0",
|
|
37
|
+
"@derwinjs/sdk": "0.11.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@vitest/coverage-v8": "^2.1.9",
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
-- Sprint 11 Phase 3 (QAP-113) — RUM / Web Vitals samples.
|
|
2
|
+
--
|
|
3
|
+
-- Stores consumer-pushed Real User Monitoring (RUM) samples emitted by the
|
|
4
|
+
-- consumer app's web-vitals hooks. One row per (projectId, metric, capturedAt)
|
|
5
|
+
-- — there is no uniqueness constraint because the same metric + URL may fire
|
|
6
|
+
-- many times per session. Baseline statistics (p50/p75/p95) are derived on
|
|
7
|
+
-- demand from this row set rather than maintained as a rolling aggregate.
|
|
8
|
+
--
|
|
9
|
+
-- Idempotent (IF NOT EXISTS) — safe to re-run on environments where the
|
|
10
|
+
-- table or index already exists.
|
|
11
|
+
|
|
12
|
+
CREATE TABLE IF NOT EXISTS "derwin"."rum_samples" (
|
|
13
|
+
"id" TEXT NOT NULL,
|
|
14
|
+
"projectId" TEXT NOT NULL,
|
|
15
|
+
"metric" TEXT NOT NULL,
|
|
16
|
+
"value" DOUBLE PRECISION NOT NULL,
|
|
17
|
+
"rating" TEXT NOT NULL,
|
|
18
|
+
"url" TEXT NOT NULL,
|
|
19
|
+
"deviceType" TEXT,
|
|
20
|
+
"sessionId" TEXT,
|
|
21
|
+
"capturedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
22
|
+
CONSTRAINT "rum_samples_pkey" PRIMARY KEY ("id")
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
CREATE INDEX IF NOT EXISTS "rum_samples_projectId_metric_capturedAt_idx"
|
|
26
|
+
ON "derwin"."rum_samples"("projectId", "metric", "capturedAt" DESC);
|
|
27
|
+
|
|
28
|
+
ALTER TABLE "derwin"."rum_samples"
|
|
29
|
+
ADD CONSTRAINT "rum_samples_projectId_fkey"
|
|
30
|
+
FOREIGN KEY ("projectId") REFERENCES "derwin"."projects"("id")
|
|
31
|
+
ON DELETE CASCADE ON UPDATE CASCADE;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
-- Sprint 11 Phase 5 (QAP-115) — MilestoneEvent foundation.
|
|
2
|
+
--
|
|
3
|
+
-- Cross-product tracking timeline data source. Backs the Tracking Timeline
|
|
4
|
+
-- Gantt UI (Phase 6) plus future cross-product coherence features (e.g.,
|
|
5
|
+
-- conflict alerts when a release lands inside another project's freeze
|
|
6
|
+
-- window). The spec's `tenantId?` field is intentionally omitted in v1 —
|
|
7
|
+
-- per the Derwin multi-tenant boundary rule (each consumer = ONE Project
|
|
8
|
+
-- row), milestones are project-scoped only; if a downstream consumer ever
|
|
9
|
+
-- needs intra-project tenant slicing it can be added as an optional column
|
|
10
|
+
-- in a future migration without breaking the existing queries.
|
|
11
|
+
--
|
|
12
|
+
-- `kind` is held as a String column rather than a Prisma enum so consumers
|
|
13
|
+
-- can emit custom kinds (alongside the canonical
|
|
14
|
+
-- release / freeze / demo / launch) without requiring a migration. The
|
|
15
|
+
-- conflict-detection helpers in `@derwinjs/core` validate the value at
|
|
16
|
+
-- runtime where it matters.
|
|
17
|
+
--
|
|
18
|
+
-- Two indexes:
|
|
19
|
+
-- - (projectId, startsAt ASC) supports the per-project chronological
|
|
20
|
+
-- listing the Phase 6 Gantt issues on every read.
|
|
21
|
+
-- - (projectId, kind) supports the swimlane-by-kind grouping the Gantt
|
|
22
|
+
-- uses to lay out releases / freezes / demos / launches.
|
|
23
|
+
|
|
24
|
+
CREATE TABLE "derwin"."milestone_events" (
|
|
25
|
+
"id" TEXT NOT NULL,
|
|
26
|
+
"projectId" TEXT NOT NULL,
|
|
27
|
+
"name" TEXT NOT NULL,
|
|
28
|
+
"description" TEXT,
|
|
29
|
+
"startsAt" TIMESTAMP(3) NOT NULL,
|
|
30
|
+
"endsAt" TIMESTAMP(3),
|
|
31
|
+
"kind" TEXT NOT NULL,
|
|
32
|
+
"createdBy" TEXT NOT NULL,
|
|
33
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
34
|
+
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
35
|
+
CONSTRAINT "milestone_events_pkey" PRIMARY KEY ("id")
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
CREATE INDEX "milestone_events_projectId_startsAt_idx" ON "derwin"."milestone_events"("projectId", "startsAt" ASC);
|
|
39
|
+
|
|
40
|
+
CREATE INDEX "milestone_events_projectId_kind_idx" ON "derwin"."milestone_events"("projectId", "kind");
|
|
41
|
+
|
|
42
|
+
ALTER TABLE "derwin"."milestone_events" ADD CONSTRAINT "milestone_events_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "derwin"."projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|