@owlmeans/marketing-consent-mongo 0.1.18-rc.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/README.md +23 -0
- package/agent-meta/manifest.json +16 -0
- package/agent-meta/skills/marketing-consent-mongo/SKILL.md +100 -0
- package/build/helper.d.ts +14 -0
- package/build/helper.d.ts.map +1 -0
- package/build/helper.js +18 -0
- package/build/helper.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +3 -0
- package/build/index.js.map +1 -0
- package/build/resource.d.ts +15 -0
- package/build/resource.d.ts.map +1 -0
- package/build/resource.js +48 -0
- package/build/resource.js.map +1 -0
- package/package.json +51 -0
- package/src/helper.ts +26 -0
- package/src/index.ts +2 -0
- package/src/resource.ts +64 -0
- package/tests/context.ts +94 -0
- package/tests/resource.spec.ts +100 -0
- package/tests/schema.spec.ts +34 -0
- package/tsconfig.json +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# @owlmeans/marketing-consent-mongo
|
|
2
|
+
|
|
3
|
+
Mongo storage for `@owlmeans/server-marketing-consent`'s two resources — `marketing-consent-state`
|
|
4
|
+
and `marketing-consent-log`. Schemas are imported from `@owlmeans/server-marketing-consent`, never
|
|
5
|
+
duplicated here.
|
|
6
|
+
|
|
7
|
+
See `.agents/skills/marketing-consent-mongo/SKILL.md` for usage.
|
|
8
|
+
|
|
9
|
+
<!-- owlmeans:agent-guidance:start -->
|
|
10
|
+
## Agent guidance
|
|
11
|
+
|
|
12
|
+
This package ships embedded agent skills under `agent-meta/`. After installing your
|
|
13
|
+
`@owlmeans/*` packages, run the OwlMeans agent-skills installer to place them into
|
|
14
|
+
your project's skill store (`.agents/skills/`):
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.33
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The embedded files are version-matched to this package release. Do not edit them
|
|
21
|
+
directly — they are regenerated on each publish. To contribute guidance edits,
|
|
22
|
+
open a PR against the source monorepo.
|
|
23
|
+
<!-- owlmeans:agent-guidance:end -->
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 2,
|
|
3
|
+
"package": "@owlmeans/marketing-consent-mongo",
|
|
4
|
+
"version": "0.1.18-rc.0",
|
|
5
|
+
"generatedAt": "2026-09-22T17:37:10.924Z",
|
|
6
|
+
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
|
+
"entries": [
|
|
8
|
+
{
|
|
9
|
+
"kind": "skill",
|
|
10
|
+
"name": "marketing-consent-mongo",
|
|
11
|
+
"category": "package-specific",
|
|
12
|
+
"file": "skills/marketing-consent-mongo/SKILL.md",
|
|
13
|
+
"canonicalPath": ".agents/skills/marketing-consent-mongo/SKILL.md"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: marketing-consent-mongo
|
|
3
|
+
description: How to use @owlmeans/marketing-consent-mongo — Mongo storage for @owlmeans/server-marketing-consent's two resources (marketing-consent-state, marketing-consent-log). Auto-invoked when registering marketing-consent resources on a Mongo-backed platform context, or touching the two Mongo indexes/schemas this package builds.
|
|
4
|
+
user-invocable: false
|
|
5
|
+
---
|
|
6
|
+
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
7
|
+
|
|
8
|
+
# @owlmeans/marketing-consent-mongo
|
|
9
|
+
|
|
10
|
+
**Layer:** Infra
|
|
11
|
+
**Install:** `"@owlmeans/marketing-consent-mongo": "^0.1.18-rc.0"` in `dependencies` (peers `mongodb`, `ajv`)
|
|
12
|
+
|
|
13
|
+
Mongo storage for `@owlmeans/server-marketing-consent`'s two resources —
|
|
14
|
+
`marketing-consent-state` (`RES_MARKETING_CONSENT_STATE`) and `marketing-consent-log`
|
|
15
|
+
(`RES_MARKETING_CONSENT_LOG`) — the same relationship `@owlmeans/server-auth-identity` has to
|
|
16
|
+
its own Mongo resources. Use it on the platform itself (Viable runs Mongo); a generated target
|
|
17
|
+
project uses the Postgres sibling, `@owlmeans/marketing-consent-postgres`, instead.
|
|
18
|
+
|
|
19
|
+
## Key Exports
|
|
20
|
+
|
|
21
|
+
| Export | Description |
|
|
22
|
+
|--------|-------------|
|
|
23
|
+
| `appendMarketingConsentMongo(ctx, opts?)` | Registers both resources — a no-op per resource if it is already registered (`ctx.hasResource`), so a second call never throws or re-registers |
|
|
24
|
+
| `makeMarketingConsentStateMongo(dbAlias?, serviceAlias?)` | Mongo resource for `MarketingConsentStateRecord` |
|
|
25
|
+
| `makeMarketingConsentLogMongo(dbAlias?, serviceAlias?)` | Mongo resource for `MarketingConsentLogRecord` |
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { appendMarketingConsentMongo } from '@owlmeans/marketing-consent-mongo'
|
|
29
|
+
import { appendMarketingConsentService } from '@owlmeans/server-marketing-consent'
|
|
30
|
+
|
|
31
|
+
appendMarketingConsentMongo(context) // register the two resources first
|
|
32
|
+
appendMarketingConsentService(context) // then the service that resolves them by alias
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Why the schema is imported, not duplicated
|
|
36
|
+
|
|
37
|
+
Both makers assign `resource.schema` from `MarketingConsentStateSchema`/`MarketingConsentLogSchema`
|
|
38
|
+
— `@owlmeans/server-marketing-consent`'s own AJV schemas — never a local redeclaration. That
|
|
39
|
+
package's skill already explains why `decisions` is an ARRAY and not an object keyed by consent
|
|
40
|
+
key (a dotted key like `marketing.email` is read as a PATH by Mongo dot-notation); this package
|
|
41
|
+
just stores that shape, it does not reshape it.
|
|
42
|
+
|
|
43
|
+
## The one adjustment: `id` is dropped from the Mongo validator's `required`
|
|
44
|
+
|
|
45
|
+
`MarketingConsentStateSchema`/`MarketingConsentLogSchema` correctly list `id` in `required` — that
|
|
46
|
+
is the right contract for `@owlmeans/server-marketing-consent`'s own AJV validation of a
|
|
47
|
+
fully-formed record, where `id` is always present after `mongo-resource` demarshals `_id`. Handed
|
|
48
|
+
to `mongo-resource` **as-is**, though, that same `required: ['id', ...]` becomes the collection's
|
|
49
|
+
`$jsonSchema` validator — and a Mongo document never carries a literal `id` field, only `_id`.
|
|
50
|
+
Verified against a real (embedded) MongoDB while building this package: assigning the schema
|
|
51
|
+
unmodified makes every `create()` fail with `MongoServerError: Document failed validation`
|
|
52
|
+
(`missingProperties: ["id"]`), because the document `mongo-resource` actually inserts has no `id`
|
|
53
|
+
key for the validator to find.
|
|
54
|
+
|
|
55
|
+
`resource.ts`'s private `forMongoValidator(schema)` fixes this the same way
|
|
56
|
+
`@owlmeans/server-payment`'s own hand-written Mongo schemas already do it — keep `id` in
|
|
57
|
+
`properties` (nullable/optional there is fine) but drop it from `required` — by shallow-cloning
|
|
58
|
+
the schema and filtering `required`. `properties` stays the exact same object the shared schema
|
|
59
|
+
exports; nothing about the record shape is duplicated or reinterpreted, only the Mongo-specific
|
|
60
|
+
`required` list is narrowed. `tests/schema.spec.ts` asserts this precisely: `resource.schema.
|
|
61
|
+
properties` is reference-equal to the shared schema's `properties`, and `required` equals the
|
|
62
|
+
shared list minus `id`.
|
|
63
|
+
|
|
64
|
+
This is a general gap in `@owlmeans/mongo-resource`'s schema→validator conversion (`schemaToMongoSchema`
|
|
65
|
+
carries `required` through verbatim, with no special case for `id`), not something specific to
|
|
66
|
+
marketing-consent — any future package that assigns a shared "full record" AJV schema straight to
|
|
67
|
+
a `MongoResource.schema` will hit the same failure. This package works around it locally; it does
|
|
68
|
+
not fix `mongo-resource` itself.
|
|
69
|
+
|
|
70
|
+
## The two indexes
|
|
71
|
+
|
|
72
|
+
- `idx_mc_state_subject` on `{ subject: 1 }`, **unique** — one current-state record per person
|
|
73
|
+
(`subject` is already `subjectKey(subject)`, so this is a collection-level backstop against a
|
|
74
|
+
create race, not the primary uniqueness mechanism).
|
|
75
|
+
- `idx_mc_state_user` on `{ userId: 1 }` — lookups by account, independent of `subject`'s
|
|
76
|
+
entity-scoping.
|
|
77
|
+
- `idx_mc_log_subject_key` on `{ subject: 1, key: 1, decidedAt: -1 }` — the log's one real read,
|
|
78
|
+
"the latest decision for this subject and key", served straight off the index.
|
|
79
|
+
- `idx_mc_log_user` on `{ userId: 1, decidedAt: -1 }` — an account's decision history in order.
|
|
80
|
+
|
|
81
|
+
## Testing
|
|
82
|
+
|
|
83
|
+
`tests/schema.spec.ts` needs no live Mongo — the schema-wiring assertions above. `tests/
|
|
84
|
+
resource.spec.ts` is Category C (`/testing-integration`): gated on `MONGO_URL`, self-skips
|
|
85
|
+
cleanly when unreachable. It boots a real `ServerContext` through `tests/context.ts` (mirrors
|
|
86
|
+
`@owlmeans/mongo`'s and `@owlmeans/server-payment`'s own harness), registers both resources via
|
|
87
|
+
`appendMarketingConsentMongo`, and covers: a state record with a dotted key inside `decisions`
|
|
88
|
+
round-tripping verbatim, the unique index rejecting a second state record for the same `subject`,
|
|
89
|
+
a log record, and `appendMarketingConsentMongo` being a true no-op — `hasResource` true and the
|
|
90
|
+
same resource instances — the second time it runs.
|
|
91
|
+
|
|
92
|
+
## Related
|
|
93
|
+
|
|
94
|
+
- `@owlmeans/server-marketing-consent` — the two record shapes/schemas, the service that resolves
|
|
95
|
+
these resources by alias, and why `decisions` is an array
|
|
96
|
+
- `@owlmeans/mongo-resource` — the maker (`makeMongoResource`), `index()`, schema→validator
|
|
97
|
+
conversion
|
|
98
|
+
- `@owlmeans/server-auth-identity` — the `appendXResources(ctx, dbAlias?)` shape this package's
|
|
99
|
+
`appendMarketingConsentMongo` copies
|
|
100
|
+
- `@owlmeans/marketing-consent-postgres` — the Postgres sibling for generated target projects
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { BasicContext } from '@owlmeans/context';
|
|
2
|
+
export interface MarketingConsentMongoOptions {
|
|
3
|
+
dbAlias?: string;
|
|
4
|
+
serviceAlias?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Registers the two Mongo resources `@owlmeans/server-marketing-consent`'s `MarketingConsentService`
|
|
8
|
+
* resolves by alias. No-op per resource if it is already registered, the same guard
|
|
9
|
+
* `@owlmeans/server-auth-identity`'s `appendAuthIdentityResources` uses for its own services — so a
|
|
10
|
+
* second call (an app composing several `append*` helpers that all happen to touch this alias)
|
|
11
|
+
* never re-registers or throws.
|
|
12
|
+
*/
|
|
13
|
+
export declare const appendMarketingConsentMongo: (ctx: BasicContext<any>, opts?: MarketingConsentMongoOptions) => void;
|
|
14
|
+
//# sourceMappingURL=helper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helper.d.ts","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAIrD,MAAM,WAAW,4BAA4B;IAC3C,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,QACjC,YAAY,CAAC,GAAG,CAAC,SAAS,4BAA4B,KAC1D,IAOF,CAAA"}
|
package/build/helper.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { RES_MARKETING_CONSENT_LOG, RES_MARKETING_CONSENT_STATE } from '@owlmeans/server-marketing-consent';
|
|
2
|
+
import { makeMarketingConsentLogMongo, makeMarketingConsentStateMongo } from './resource.js';
|
|
3
|
+
/**
|
|
4
|
+
* Registers the two Mongo resources `@owlmeans/server-marketing-consent`'s `MarketingConsentService`
|
|
5
|
+
* resolves by alias. No-op per resource if it is already registered, the same guard
|
|
6
|
+
* `@owlmeans/server-auth-identity`'s `appendAuthIdentityResources` uses for its own services — so a
|
|
7
|
+
* second call (an app composing several `append*` helpers that all happen to touch this alias)
|
|
8
|
+
* never re-registers or throws.
|
|
9
|
+
*/
|
|
10
|
+
export const appendMarketingConsentMongo = (ctx, opts) => {
|
|
11
|
+
if (!ctx.hasResource(RES_MARKETING_CONSENT_STATE)) {
|
|
12
|
+
ctx.registerResource(makeMarketingConsentStateMongo(opts?.dbAlias, opts?.serviceAlias));
|
|
13
|
+
}
|
|
14
|
+
if (!ctx.hasResource(RES_MARKETING_CONSENT_LOG)) {
|
|
15
|
+
ctx.registerResource(makeMarketingConsentLogMongo(opts?.dbAlias, opts?.serviceAlias));
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=helper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helper.js","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,yBAAyB,EAAE,2BAA2B,EAAE,MAAM,oCAAoC,CAAA;AAC3G,OAAO,EAAE,4BAA4B,EAAE,8BAA8B,EAAE,MAAM,eAAe,CAAA;AAO5F;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,CACzC,GAAsB,EAAE,IAAmC,EACrD,EAAE;IACR,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,2BAA2B,CAAC,EAAE,CAAC;QAClD,GAAG,CAAC,gBAAgB,CAAC,8BAA8B,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,CAAC,CAAC,CAAA;IACzF,CAAC;IACD,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,yBAAyB,CAAC,EAAE,CAAC;QAChD,GAAG,CAAC,gBAAgB,CAAC,4BAA4B,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,CAAC,CAAC,CAAA;IACvF,CAAC;AACH,CAAC,CAAA"}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAA;AAC7B,cAAc,aAAa,CAAA"}
|
package/build/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAA;AAC7B,cAAc,aAAa,CAAA"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { MongoResource } from '@owlmeans/mongo-resource';
|
|
2
|
+
import type { MarketingConsentLogRecord, MarketingConsentStateRecord } from '@owlmeans/server-marketing-consent';
|
|
3
|
+
/**
|
|
4
|
+
* One record per subject (`id` = `subjectKey(subject)`, already unique by construction), so the
|
|
5
|
+
* unique index on `subject` is the collection-level backstop against a create race producing two
|
|
6
|
+
* current-state rows for the same person.
|
|
7
|
+
*/
|
|
8
|
+
export declare const makeMarketingConsentStateMongo: (dbAlias?: string, serviceAlias?: string) => MongoResource<MarketingConsentStateRecord>;
|
|
9
|
+
/**
|
|
10
|
+
* Append-only evidence log. The compound index serves the one read this record shape exists
|
|
11
|
+
* for — "the latest decision for this subject and key" — sorted straight off the index rather
|
|
12
|
+
* than folded from a full per-subject scan.
|
|
13
|
+
*/
|
|
14
|
+
export declare const makeMarketingConsentLogMongo: (dbAlias?: string, serviceAlias?: string) => MongoResource<MarketingConsentLogRecord>;
|
|
15
|
+
//# sourceMappingURL=resource.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAK7D,OAAO,KAAK,EAAE,yBAAyB,EAAE,2BAA2B,EAAE,MAAM,oCAAoC,CAAA;AAyBhH;;;;GAIG;AACH,eAAO,MAAM,8BAA8B,aAC/B,MAAM,iBAAiB,MAAM,KACtC,aAAa,CAAC,2BAA2B,CAQ3C,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,4BAA4B,aAC7B,MAAM,iBAAiB,MAAM,KACtC,aAAa,CAAC,yBAAyB,CAQzC,CAAA"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { makeMongoResource } from '@owlmeans/mongo-resource';
|
|
2
|
+
import { MarketingConsentLogSchema, MarketingConsentStateSchema, RES_MARKETING_CONSENT_LOG, RES_MARKETING_CONSENT_STATE } from '@owlmeans/server-marketing-consent';
|
|
3
|
+
/**
|
|
4
|
+
* A stored Mongo document never carries a literal `id` field — only `_id`, which mongo-resource
|
|
5
|
+
* derives `id` FROM on every read and never writes on `create()`. The shared schema's `required`
|
|
6
|
+
* list is correct for `@owlmeans/server-marketing-consent`'s own AJV validation of a fully-formed
|
|
7
|
+
* record (post-demarshal, where `id` is always present) — but handed to `mongo-resource` verbatim
|
|
8
|
+
* as the collection's `$jsonSchema` validator, it makes EVERY `create()` fail with "Document
|
|
9
|
+
* failed validation" (`missingProperties: ["id"]`), because the document actually inserted never
|
|
10
|
+
* has that key. Verified against a real (embedded) MongoDB while building this package: assigning
|
|
11
|
+
* `MarketingConsentStateSchema` as-is reproduces the failure, dropping `id` from `required` fixes
|
|
12
|
+
* it. `properties` (and everything else) stays the exact same object `@owlmeans/
|
|
13
|
+
* server-marketing-consent` exports — nothing here is duplicated, only the Mongo-specific
|
|
14
|
+
* `required` list is narrowed, the same `id`-not-required shape `@owlmeans/server-payment`'s own
|
|
15
|
+
* hand-written Mongo schemas already use for their records.
|
|
16
|
+
*/
|
|
17
|
+
const forMongoValidator = (schema) => {
|
|
18
|
+
const source = schema;
|
|
19
|
+
if (source.required == null) {
|
|
20
|
+
return schema;
|
|
21
|
+
}
|
|
22
|
+
return { ...schema, required: source.required.filter(field => field !== 'id') };
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* One record per subject (`id` = `subjectKey(subject)`, already unique by construction), so the
|
|
26
|
+
* unique index on `subject` is the collection-level backstop against a create race producing two
|
|
27
|
+
* current-state rows for the same person.
|
|
28
|
+
*/
|
|
29
|
+
export const makeMarketingConsentStateMongo = (dbAlias, serviceAlias) => {
|
|
30
|
+
const resource = makeMongoResource(RES_MARKETING_CONSENT_STATE, dbAlias, serviceAlias);
|
|
31
|
+
resource.schema = forMongoValidator(MarketingConsentStateSchema);
|
|
32
|
+
resource.index('idx_mc_state_subject', { subject: 1 }, { unique: true });
|
|
33
|
+
resource.index('idx_mc_state_user', { userId: 1 });
|
|
34
|
+
return resource;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Append-only evidence log. The compound index serves the one read this record shape exists
|
|
38
|
+
* for — "the latest decision for this subject and key" — sorted straight off the index rather
|
|
39
|
+
* than folded from a full per-subject scan.
|
|
40
|
+
*/
|
|
41
|
+
export const makeMarketingConsentLogMongo = (dbAlias, serviceAlias) => {
|
|
42
|
+
const resource = makeMongoResource(RES_MARKETING_CONSENT_LOG, dbAlias, serviceAlias);
|
|
43
|
+
resource.schema = forMongoValidator(MarketingConsentLogSchema);
|
|
44
|
+
resource.index('idx_mc_log_subject_key', { subject: 1, key: 1, decidedAt: -1 });
|
|
45
|
+
resource.index('idx_mc_log_user', { userId: 1, decidedAt: -1 });
|
|
46
|
+
return resource;
|
|
47
|
+
};
|
|
48
|
+
//# sourceMappingURL=resource.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resource.js","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA;AAE5D,OAAO,EACL,yBAAyB,EAAE,2BAA2B,EAAE,yBAAyB,EACjF,2BAA2B,EAC5B,MAAM,oCAAoC,CAAA;AAI3C;;;;;;;;;;;;;GAaG;AACH,MAAM,iBAAiB,GAAG,CAAC,MAAiB,EAAa,EAAE;IACzD,MAAM,MAAM,GAAG,MAAiC,CAAA;IAChD,IAAI,MAAM,CAAC,QAAQ,IAAI,IAAI,EAAE,CAAC;QAC5B,OAAO,MAAM,CAAA;IACf,CAAC;IACD,OAAO,EAAE,GAAI,MAAiB,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,KAAK,IAAI,CAAC,EAAe,CAAA;AAC1G,CAAC,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,8BAA8B,GAAG,CAC5C,OAAgB,EAAE,YAAqB,EACK,EAAE;IAC9C,MAAM,QAAQ,GAAG,iBAAiB,CAChC,2BAA2B,EAAE,OAAO,EAAE,YAAY,CACnD,CAAA;IACD,QAAQ,CAAC,MAAM,GAAG,iBAAiB,CAAC,2BAA2B,CAAC,CAAA;IAChE,QAAQ,CAAC,KAAK,CAAC,sBAAsB,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;IACxE,QAAQ,CAAC,KAAK,CAAC,mBAAmB,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAA;IAClD,OAAO,QAAQ,CAAA;AACjB,CAAC,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,4BAA4B,GAAG,CAC1C,OAAgB,EAAE,YAAqB,EACG,EAAE;IAC5C,MAAM,QAAQ,GAAG,iBAAiB,CAChC,yBAAyB,EAAE,OAAO,EAAE,YAAY,CACjD,CAAA;IACD,QAAQ,CAAC,MAAM,GAAG,iBAAiB,CAAC,yBAAyB,CAAC,CAAA;IAC9D,QAAQ,CAAC,KAAK,CAAC,wBAAwB,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,EAAE,CAAC,CAAA;IAC/E,QAAQ,CAAC,KAAK,CAAC,iBAAiB,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,EAAE,CAAC,CAAA;IAC/D,OAAO,QAAQ,CAAA;AACjB,CAAC,CAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@owlmeans/marketing-consent-mongo",
|
|
3
|
+
"version": "0.1.18-rc.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "Mongo storage for @owlmeans/server-marketing-consent's two resources (marketing-consent-state, marketing-consent-log) — schemas imported, never duplicated. For the platform; generated target projects use @owlmeans/marketing-consent-postgres.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc -b",
|
|
9
|
+
"dev": "sleep 2 && nodemon -e ts,tsx,json --watch src --exec \"tsc -p ./tsconfig.json\"",
|
|
10
|
+
"watch": "tsc -b -w --preserveWatchOutput --pretty",
|
|
11
|
+
"test": "bun test ./tests"
|
|
12
|
+
},
|
|
13
|
+
"main": "build/index.js",
|
|
14
|
+
"module": "build/index.js",
|
|
15
|
+
"types": "build/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"import": "./build/index.js",
|
|
19
|
+
"require": "./build/index.js",
|
|
20
|
+
"default": "./build/index.js",
|
|
21
|
+
"module": "./build/index.js",
|
|
22
|
+
"types": "./build/index.d.ts"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@owlmeans/context": "^0.1.18-rc.29",
|
|
27
|
+
"@owlmeans/marketing-consent": "^0.1.18-rc.0",
|
|
28
|
+
"@owlmeans/mongo-resource": "^0.1.18-rc.32",
|
|
29
|
+
"@owlmeans/resource": "^0.1.18-rc.31",
|
|
30
|
+
"@owlmeans/server-marketing-consent": "^0.1.18-rc.0"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"ajv": "*",
|
|
34
|
+
"mongodb": "*"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@owlmeans/dep-config": "workspace:*",
|
|
38
|
+
"@owlmeans/mongo": "^0.1.18-rc.31",
|
|
39
|
+
"@owlmeans/server-context": "^0.1.18-rc.32",
|
|
40
|
+
"@owlmeans/test": "^0.1.18-rc.29",
|
|
41
|
+
"@owlmeans/test-integration": "^0.1.18-rc.26",
|
|
42
|
+
"@types/bun": "^1.4.0",
|
|
43
|
+
"ajv": "^8.17.1",
|
|
44
|
+
"mongodb": "^7.5.0",
|
|
45
|
+
"nodemon": "^3.1.14",
|
|
46
|
+
"typescript": "^7.0.2"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/helper.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { BasicContext } from '@owlmeans/context'
|
|
2
|
+
import { RES_MARKETING_CONSENT_LOG, RES_MARKETING_CONSENT_STATE } from '@owlmeans/server-marketing-consent'
|
|
3
|
+
import { makeMarketingConsentLogMongo, makeMarketingConsentStateMongo } from './resource.js'
|
|
4
|
+
|
|
5
|
+
export interface MarketingConsentMongoOptions {
|
|
6
|
+
dbAlias?: string
|
|
7
|
+
serviceAlias?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Registers the two Mongo resources `@owlmeans/server-marketing-consent`'s `MarketingConsentService`
|
|
12
|
+
* resolves by alias. No-op per resource if it is already registered, the same guard
|
|
13
|
+
* `@owlmeans/server-auth-identity`'s `appendAuthIdentityResources` uses for its own services — so a
|
|
14
|
+
* second call (an app composing several `append*` helpers that all happen to touch this alias)
|
|
15
|
+
* never re-registers or throws.
|
|
16
|
+
*/
|
|
17
|
+
export const appendMarketingConsentMongo = (
|
|
18
|
+
ctx: BasicContext<any>, opts?: MarketingConsentMongoOptions
|
|
19
|
+
): void => {
|
|
20
|
+
if (!ctx.hasResource(RES_MARKETING_CONSENT_STATE)) {
|
|
21
|
+
ctx.registerResource(makeMarketingConsentStateMongo(opts?.dbAlias, opts?.serviceAlias))
|
|
22
|
+
}
|
|
23
|
+
if (!ctx.hasResource(RES_MARKETING_CONSENT_LOG)) {
|
|
24
|
+
ctx.registerResource(makeMarketingConsentLogMongo(opts?.dbAlias, opts?.serviceAlias))
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/index.ts
ADDED
package/src/resource.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { makeMongoResource } from '@owlmeans/mongo-resource'
|
|
2
|
+
import type { MongoResource } from '@owlmeans/mongo-resource'
|
|
3
|
+
import {
|
|
4
|
+
MarketingConsentLogSchema, MarketingConsentStateSchema, RES_MARKETING_CONSENT_LOG,
|
|
5
|
+
RES_MARKETING_CONSENT_STATE
|
|
6
|
+
} from '@owlmeans/server-marketing-consent'
|
|
7
|
+
import type { MarketingConsentLogRecord, MarketingConsentStateRecord } from '@owlmeans/server-marketing-consent'
|
|
8
|
+
import type { AnySchema } from 'ajv'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A stored Mongo document never carries a literal `id` field — only `_id`, which mongo-resource
|
|
12
|
+
* derives `id` FROM on every read and never writes on `create()`. The shared schema's `required`
|
|
13
|
+
* list is correct for `@owlmeans/server-marketing-consent`'s own AJV validation of a fully-formed
|
|
14
|
+
* record (post-demarshal, where `id` is always present) — but handed to `mongo-resource` verbatim
|
|
15
|
+
* as the collection's `$jsonSchema` validator, it makes EVERY `create()` fail with "Document
|
|
16
|
+
* failed validation" (`missingProperties: ["id"]`), because the document actually inserted never
|
|
17
|
+
* has that key. Verified against a real (embedded) MongoDB while building this package: assigning
|
|
18
|
+
* `MarketingConsentStateSchema` as-is reproduces the failure, dropping `id` from `required` fixes
|
|
19
|
+
* it. `properties` (and everything else) stays the exact same object `@owlmeans/
|
|
20
|
+
* server-marketing-consent` exports — nothing here is duplicated, only the Mongo-specific
|
|
21
|
+
* `required` list is narrowed, the same `id`-not-required shape `@owlmeans/server-payment`'s own
|
|
22
|
+
* hand-written Mongo schemas already use for their records.
|
|
23
|
+
*/
|
|
24
|
+
const forMongoValidator = (schema: AnySchema): AnySchema => {
|
|
25
|
+
const source = schema as { required?: string[] }
|
|
26
|
+
if (source.required == null) {
|
|
27
|
+
return schema
|
|
28
|
+
}
|
|
29
|
+
return { ...(schema as object), required: source.required.filter(field => field !== 'id') } as AnySchema
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* One record per subject (`id` = `subjectKey(subject)`, already unique by construction), so the
|
|
34
|
+
* unique index on `subject` is the collection-level backstop against a create race producing two
|
|
35
|
+
* current-state rows for the same person.
|
|
36
|
+
*/
|
|
37
|
+
export const makeMarketingConsentStateMongo = (
|
|
38
|
+
dbAlias?: string, serviceAlias?: string
|
|
39
|
+
): MongoResource<MarketingConsentStateRecord> => {
|
|
40
|
+
const resource = makeMongoResource<MarketingConsentStateRecord>(
|
|
41
|
+
RES_MARKETING_CONSENT_STATE, dbAlias, serviceAlias
|
|
42
|
+
)
|
|
43
|
+
resource.schema = forMongoValidator(MarketingConsentStateSchema)
|
|
44
|
+
resource.index('idx_mc_state_subject', { subject: 1 }, { unique: true })
|
|
45
|
+
resource.index('idx_mc_state_user', { userId: 1 })
|
|
46
|
+
return resource
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Append-only evidence log. The compound index serves the one read this record shape exists
|
|
51
|
+
* for — "the latest decision for this subject and key" — sorted straight off the index rather
|
|
52
|
+
* than folded from a full per-subject scan.
|
|
53
|
+
*/
|
|
54
|
+
export const makeMarketingConsentLogMongo = (
|
|
55
|
+
dbAlias?: string, serviceAlias?: string
|
|
56
|
+
): MongoResource<MarketingConsentLogRecord> => {
|
|
57
|
+
const resource = makeMongoResource<MarketingConsentLogRecord>(
|
|
58
|
+
RES_MARKETING_CONSENT_LOG, dbAlias, serviceAlias
|
|
59
|
+
)
|
|
60
|
+
resource.schema = forMongoValidator(MarketingConsentLogSchema)
|
|
61
|
+
resource.index('idx_mc_log_subject_key', { subject: 1, key: 1, decidedAt: -1 })
|
|
62
|
+
resource.index('idx_mc_log_user', { userId: 1, decidedAt: -1 })
|
|
63
|
+
return resource
|
|
64
|
+
}
|
package/tests/context.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { mongoGate, randomNamespace } from '@owlmeans/test-integration'
|
|
2
|
+
import type { IntegrationGate, MongoEnv } from '@owlmeans/test-integration'
|
|
3
|
+
import { appendMongo } from '@owlmeans/mongo'
|
|
4
|
+
import type { MongoDbService } from '@owlmeans/mongo-resource'
|
|
5
|
+
import { config, makeServerContext } from '@owlmeans/server-context'
|
|
6
|
+
import type { ServerConfig, ServerContext } from '@owlmeans/server-context'
|
|
7
|
+
import { MongoClient } from 'mongodb'
|
|
8
|
+
import { appendMarketingConsentMongo } from '../src/helper.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Category-C harness: a real `ServerContext` over a real Mongo database, with the two resources
|
|
12
|
+
* registered through `appendMarketingConsentMongo` exactly as a consuming app would — mirrors
|
|
13
|
+
* `@owlmeans/mongo`'s and `@owlmeans/server-payment`'s own `tests/context.ts`. One database per
|
|
14
|
+
* suite, dropped by that suite's own teardown.
|
|
15
|
+
*/
|
|
16
|
+
export const gate: IntegrationGate<MongoEnv> = mongoGate()
|
|
17
|
+
|
|
18
|
+
const url = (): string => gate.env.MONGO_URL as string
|
|
19
|
+
|
|
20
|
+
/** Short lived client for setup and assertions outside any context. */
|
|
21
|
+
export const raw = async <R>(fn: (client: MongoClient) => Promise<R>): Promise<R> => {
|
|
22
|
+
const client = new MongoClient(url())
|
|
23
|
+
try {
|
|
24
|
+
await client.connect()
|
|
25
|
+
|
|
26
|
+
return await fn(client)
|
|
27
|
+
} finally {
|
|
28
|
+
await client.close().catch(() => undefined)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface Booted {
|
|
33
|
+
context: ServerContext<ServerConfig>
|
|
34
|
+
mongo: MongoDbService
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface MongoSuite {
|
|
38
|
+
database: string
|
|
39
|
+
boot: () => Promise<Booted>
|
|
40
|
+
teardown: () => Promise<void>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const makeSuite = (label: string): MongoSuite => {
|
|
44
|
+
const prefix = process.env.MONGO_TEST_DB_PREFIX ?? 'omt'
|
|
45
|
+
const database = randomNamespace(`${prefix}_${label}`)
|
|
46
|
+
const clients: MongoClient[] = []
|
|
47
|
+
|
|
48
|
+
const boot = async (): Promise<Booted> => {
|
|
49
|
+
const parsed = new URL(url())
|
|
50
|
+
const cfg: ServerConfig = config('mc-mongo-test', {
|
|
51
|
+
dbs: [{
|
|
52
|
+
service: 'mongo',
|
|
53
|
+
alias: 'mongo',
|
|
54
|
+
host: decodeURIComponent(parsed.hostname).replace(/^\[|\]$/g, ''),
|
|
55
|
+
port: parsed.port !== '' ? parseInt(parsed.port, 10) : 27017,
|
|
56
|
+
schema: database,
|
|
57
|
+
...(parsed.username !== ''
|
|
58
|
+
? { user: decodeURIComponent(parsed.username), secret: decodeURIComponent(parsed.password) }
|
|
59
|
+
: {})
|
|
60
|
+
}]
|
|
61
|
+
} as Partial<ServerConfig>)
|
|
62
|
+
|
|
63
|
+
const context = makeServerContext(cfg) as ServerContext<ServerConfig>
|
|
64
|
+
appendMongo(context)
|
|
65
|
+
appendMarketingConsentMongo(context)
|
|
66
|
+
|
|
67
|
+
context.configure()
|
|
68
|
+
const mongo = context.service<MongoDbService>('mongo')
|
|
69
|
+
try {
|
|
70
|
+
await context.init()
|
|
71
|
+
} finally {
|
|
72
|
+
for (const client of Object.values(mongo.clients ?? {}) as MongoClient[]) {
|
|
73
|
+
if (!clients.includes(client)) {
|
|
74
|
+
clients.push(client)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { context, mongo }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const teardown = async (): Promise<void> => {
|
|
83
|
+
if (gate.skip) {
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
await raw(async client => { await client.db(database).dropDatabase() })
|
|
87
|
+
.catch(error => console.error(`marketing-consent-mongo test teardown (${database}):`, error))
|
|
88
|
+
while (clients.length > 0) {
|
|
89
|
+
await clients.pop()?.close().catch(() => undefined)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { database, boot, teardown }
|
|
94
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { afterAll, describe, expect, test } from 'bun:test'
|
|
2
|
+
import type { MongoResource } from '@owlmeans/mongo-resource'
|
|
3
|
+
import type { MarketingConsentLogRecord, MarketingConsentStateRecord } from '@owlmeans/server-marketing-consent'
|
|
4
|
+
import { RES_MARKETING_CONSENT_LOG, RES_MARKETING_CONSENT_STATE } from '@owlmeans/server-marketing-consent'
|
|
5
|
+
import { appendMarketingConsentMongo } from '../src/helper.js'
|
|
6
|
+
import { gate, makeSuite } from './context.js'
|
|
7
|
+
|
|
8
|
+
const suite = makeSuite('mc-resource')
|
|
9
|
+
const it = gate.skip ? test.skip : test
|
|
10
|
+
|
|
11
|
+
const decision = (key: string) => ({
|
|
12
|
+
key, granted: true, revisedAt: '2026-01-01T00:00:00.000Z', mode: 'opt-in' as const,
|
|
13
|
+
decidedAt: '2026-01-01T00:00:00.000Z', source: 'settings' as const,
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
describe('@owlmeans/marketing-consent-mongo — resources against a real Mongo', () => {
|
|
17
|
+
if (gate.skip) {
|
|
18
|
+
test.skip(gate.reason ?? 'mongo gate closed', () => {})
|
|
19
|
+
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
afterAll(async () => {
|
|
24
|
+
await suite.teardown()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('stores a state record with a dotted consent key and reads it back verbatim', async () => {
|
|
28
|
+
const { context } = await suite.boot()
|
|
29
|
+
const state = context.resource<MongoResource<MarketingConsentStateRecord>>(RES_MARKETING_CONSENT_STATE)
|
|
30
|
+
|
|
31
|
+
const now = new Date().toISOString()
|
|
32
|
+
const created = await state.create({
|
|
33
|
+
subject: 'subject-1',
|
|
34
|
+
userId: 'user-1',
|
|
35
|
+
// A dotted key ("marketing.email") is illegal as a Mongo FIELD name but fine as a plain
|
|
36
|
+
// STRING VALUE inside an array element — the exact reason `decisions` is an array and not
|
|
37
|
+
// an object keyed by consent key (see @owlmeans/server-marketing-consent's model.ts).
|
|
38
|
+
decisions: [decision('marketing.email'), decision('trackers.advertising')],
|
|
39
|
+
createdAt: now,
|
|
40
|
+
updatedAt: now,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
expect(typeof created.id).toBe('string')
|
|
44
|
+
|
|
45
|
+
const reread = await state.get(created.id!)
|
|
46
|
+
expect(reread.subject).toBe('subject-1')
|
|
47
|
+
expect(reread.userId).toBe('user-1')
|
|
48
|
+
expect(reread.decisions).toHaveLength(2)
|
|
49
|
+
expect(reread.decisions.map(d => d.key)).toEqual(['marketing.email', 'trackers.advertising'])
|
|
50
|
+
expect(reread.decisions[0]).toMatchObject(decision('marketing.email'))
|
|
51
|
+
expect(reread.createdAt).toBe(now)
|
|
52
|
+
expect(reread.updatedAt).toBe(now)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('rejects a second state record for the same subject', async () => {
|
|
56
|
+
const { context } = await suite.boot()
|
|
57
|
+
const state = context.resource<MongoResource<MarketingConsentStateRecord>>(RES_MARKETING_CONSENT_STATE)
|
|
58
|
+
|
|
59
|
+
const now = new Date().toISOString()
|
|
60
|
+
await state.create({
|
|
61
|
+
subject: 'subject-dup', userId: 'user-a', decisions: [], createdAt: now, updatedAt: now,
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
await expect(state.create({
|
|
65
|
+
subject: 'subject-dup', userId: 'user-b', decisions: [], createdAt: now, updatedAt: now,
|
|
66
|
+
})).rejects.toThrow()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('stores an append-only log record', async () => {
|
|
70
|
+
const { context } = await suite.boot()
|
|
71
|
+
const log = context.resource<MongoResource<MarketingConsentLogRecord>>(RES_MARKETING_CONSENT_LOG)
|
|
72
|
+
|
|
73
|
+
const now = new Date().toISOString()
|
|
74
|
+
const created = await log.create({
|
|
75
|
+
subject: 'subject-log', userId: 'user-log', kind: 'consent', key: 'marketing.email',
|
|
76
|
+
granted: true, revisedAt: now, mode: 'opt-in', decidedAt: now, source: 'settings',
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
expect(typeof created.id).toBe('string')
|
|
80
|
+
const reread = await log.get(created.id!)
|
|
81
|
+
expect(reread.key).toBe('marketing.email')
|
|
82
|
+
expect(reread.kind).toBe('consent')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('appendMarketingConsentMongo is a no-op the second time it runs', async () => {
|
|
86
|
+
const { context } = await suite.boot()
|
|
87
|
+
|
|
88
|
+
expect(context.hasResource(RES_MARKETING_CONSENT_STATE)).toBe(true)
|
|
89
|
+
expect(context.hasResource(RES_MARKETING_CONSENT_LOG)).toBe(true)
|
|
90
|
+
|
|
91
|
+
const state = context.resource<MongoResource<MarketingConsentStateRecord>>(RES_MARKETING_CONSENT_STATE)
|
|
92
|
+
const log = context.resource<MongoResource<MarketingConsentLogRecord>>(RES_MARKETING_CONSENT_LOG)
|
|
93
|
+
|
|
94
|
+
expect(() => appendMarketingConsentMongo(context)).not.toThrow()
|
|
95
|
+
|
|
96
|
+
// Same resource instances — nothing was re-registered.
|
|
97
|
+
expect(context.resource<MongoResource<MarketingConsentStateRecord>>(RES_MARKETING_CONSENT_STATE)).toBe(state)
|
|
98
|
+
expect(context.resource<MongoResource<MarketingConsentLogRecord>>(RES_MARKETING_CONSENT_LOG)).toBe(log)
|
|
99
|
+
})
|
|
100
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { MarketingConsentLogSchema, MarketingConsentStateSchema } from '@owlmeans/server-marketing-consent'
|
|
3
|
+
import { makeMarketingConsentLogMongo, makeMarketingConsentStateMongo } from '../src/resource.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Needs no live Mongo. Proves the two makers build the collection validator FROM the shared
|
|
7
|
+
* schemas in `@owlmeans/server-marketing-consent` — `properties` is the exact same object,
|
|
8
|
+
* nothing is redeclared — while narrowing `required` to drop `id`, which a stored Mongo document
|
|
9
|
+
* never carries (see the comment on `forMongoValidator` in `../src/resource.ts` for why: handing
|
|
10
|
+
* the schema to Mongo's `$jsonSchema` validator as-is makes every `create()` fail).
|
|
11
|
+
*/
|
|
12
|
+
describe('@owlmeans/marketing-consent-mongo — schema wiring', () => {
|
|
13
|
+
test('the state resource shares the state schema\'s properties and drops `id` from required', () => {
|
|
14
|
+
const resource = makeMarketingConsentStateMongo()
|
|
15
|
+
const schema = resource.schema as { properties: unknown, required: string[] }
|
|
16
|
+
|
|
17
|
+
expect(schema.properties).toBe(MarketingConsentStateSchema.properties as never)
|
|
18
|
+
expect(schema.required).toEqual(
|
|
19
|
+
(MarketingConsentStateSchema.required as string[]).filter(field => field !== 'id')
|
|
20
|
+
)
|
|
21
|
+
expect(schema.required).not.toContain('id')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('the log resource shares the log schema\'s properties and drops `id` from required', () => {
|
|
25
|
+
const resource = makeMarketingConsentLogMongo()
|
|
26
|
+
const schema = resource.schema as { properties: unknown, required: string[] }
|
|
27
|
+
|
|
28
|
+
expect(schema.properties).toBe(MarketingConsentLogSchema.properties as never)
|
|
29
|
+
expect(schema.required).toEqual(
|
|
30
|
+
(MarketingConsentLogSchema.required as string[]).filter(field => field !== 'id')
|
|
31
|
+
)
|
|
32
|
+
expect(schema.required).not.toContain('id')
|
|
33
|
+
})
|
|
34
|
+
})
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": [
|
|
3
|
+
"@owlmeans/dep-config/tsconfig.base.json",
|
|
4
|
+
"@owlmeans/dep-config/tsconfig.node.json"
|
|
5
|
+
],
|
|
6
|
+
"compilerOptions": {
|
|
7
|
+
"rootDir": "./src/",
|
|
8
|
+
"outDir": "./build/"
|
|
9
|
+
},
|
|
10
|
+
"exclude": [
|
|
11
|
+
"./dist/**/*",
|
|
12
|
+
"./build/**/*",
|
|
13
|
+
"./tests/**/*",
|
|
14
|
+
"./*.ts"
|
|
15
|
+
]
|
|
16
|
+
}
|