musora-content-services 2.178.1 → 2.180.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/.agent/decisions/2026-05-20-content-progress-ts-part-5.md +39 -0
- package/.agent/decisions/2026-08-28-mu2-1877-marketing-sanity-queries.md +38 -0
- package/.agent/decisions/2026-09-04-sanity-client-cloudflare-cache.md +37 -0
- package/.claude/settings.local.json +71 -0
- package/CHANGELOG.md +20 -0
- package/package.json +2 -1
- package/src/index.d.ts +13 -0
- package/src/index.js +13 -0
- package/src/infrastructure/http/HttpClient.ts +15 -1
- package/src/infrastructure/sanity/executors/FetchQueryExecutor.ts +1 -1
- package/src/lib/ads/coproduct.ts +57 -10
- package/src/lib/ads/either.ts +11 -1
- package/src/lib/sanity/types/marketing.d.ts +94 -0
- package/src/services/config.js +5 -0
- package/src/services/content-org/learning-paths.ts +83 -63
- package/src/services/marketing/marketing.ts +87 -0
- package/src/services/progress/collections.ts +8 -0
- package/src/services/progress/index.ts +2 -1
- package/src/services/progress/internal/bubble.ts +4 -1
- package/src/services/progress/internal/learning-path.ts +14 -0
- package/src/services/progress/internal/queries.ts +7 -3
- package/src/services/progress/mutations.ts +316 -0
- package/src/services/progress/state.ts +54 -33
- package/src/services/sanity.js +31 -17
- package/src/services/state.ts +2 -0
- package/src/services/user/management.js +8 -2
- package/src/version-info.js +1 -1
- package/tools/generate-sanity-types.cjs +212 -0
- package/.idea/codeStyles/Project.xml +0 -61
- package/.idea/codeStyles/codeStyleConfig.xml +0 -5
- package/.idea/modules.xml +0 -8
- package/.idea/musora-content-services.iml +0 -8
- package/.idea/php.xml +0 -19
- package/.idea/prettier.xml +0 -6
- package/.idea/vcs.xml +0 -7
- package/.yarn/install-state.gz +0 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
date: 2026-05-20
|
|
3
|
+
branch: refactor/content-progress-ts-part-5
|
|
4
|
+
pr: https://github.com/railroadmedia/musora-content-services/pull/981
|
|
5
|
+
status: open
|
|
6
|
+
tags: [[chore]]
|
|
7
|
+
related: [[2026-05-19-content-progress-ts-part-2]]
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Extract progress mutations and learning-path duplication guard
|
|
11
|
+
|
|
12
|
+
## Context
|
|
13
|
+
|
|
14
|
+
With the read path (`state.ts`, `collections.ts`), shared types, bubble/trickle logic (`internal/bubble.ts`), and query helpers (`internal/queries.ts`) already extracted in earlier parts of the refactor, the write path remained in the legacy `contentProgress.js`. All mutation operations (`save`, `setStatus`, `setStatusMany`, `markCompleted`, `markStarted`, `reset`) needed to move into the typed `progress/` module to complete the decomposition and enable direct test coverage.
|
|
15
|
+
|
|
16
|
+
## Decision
|
|
17
|
+
|
|
18
|
+
Introduced two new files:
|
|
19
|
+
|
|
20
|
+
- `src/services/progress/mutations.ts` — owns all write operations. `save` handles offline/online paths, filters negative-progress writes, computes bubble/trickle, and delegates LP completion actions. `setStatus`/`setStatusMany`/`reset` follow the same shape. Public `mark*` aliases provide a stable call surface. A private `requestPush` helper centralises the skipPush guard pattern.
|
|
21
|
+
- `src/services/progress/internal/learning-path.ts` — extracts `filterOutLearningPathsForDuplication`, the rule that prevents the LP root record from being written back as an a-la-carte record. Isolating it makes the invariant visible and independently testable.
|
|
22
|
+
|
|
23
|
+
Test coverage added:
|
|
24
|
+
- `test/integration/progress/mutations.test.ts` — integration tests against the in-memory WatermelonDB harness covering all public mutation functions and key scenarios (higher/lower/equal progress, skipBubbleTrickle, etc.)
|
|
25
|
+
- `test/unit/services/progress-internal/learning-path.test.ts` — unit tests for the LP duplication filter covering non-LP passthrough, LP root exclusion, and string-key coercion
|
|
26
|
+
|
|
27
|
+
## Alternatives Considered
|
|
28
|
+
|
|
29
|
+
Keeping mutations in `contentProgress.js` until the full JS→TS migration is complete was considered but rejected. The write path is high-risk and untested; extracting it into TypeScript now lets us add direct coverage and enforce types on the critical save/bubble interaction.
|
|
30
|
+
|
|
31
|
+
## Process Notes
|
|
32
|
+
|
|
33
|
+
`filterOutLearningPathsForDuplication` uses `Number(id)` on string keys from `Object.entries` to match the numeric `collection.id`. A dedicated unit test documents and guards this coercion.
|
|
34
|
+
|
|
35
|
+
## Consequences
|
|
36
|
+
|
|
37
|
+
- `Progress.*` namespace now covers both the read and write path; callers can import from `src/services/progress` for all operations
|
|
38
|
+
- The write path has direct integration test coverage for the first time
|
|
39
|
+
- `contentProgress.js` mutation functions can be deprecated and removed in a follow-up once consumers are migrated
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
date: 2026-08-28
|
|
3
|
+
branch: feat/marketing-sanity-queries
|
|
4
|
+
pr: https://github.com/railroadmedia/musora-content-services/pull/1040
|
|
5
|
+
status: open
|
|
6
|
+
tags: [[feature]]
|
|
7
|
+
related: [[2026-07-20-query-builder-tostring]]
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Marketing workspace Sanity types and query functions
|
|
11
|
+
|
|
12
|
+
## Context
|
|
13
|
+
The new marketing workspace in Sanity introduced four document types — `stats`, `practice-goal`, `testimonial` and `faq` — with no typed access from MCS. Consumers would otherwise hand-write both the GROQ and the result shapes, which drift as the schema changes.
|
|
14
|
+
|
|
15
|
+
## Decision
|
|
16
|
+
Two pieces:
|
|
17
|
+
|
|
18
|
+
- `tools/generate-sanity-types.cjs` (exposed as `npm run gen-sanity-types`) derives TypeScript declarations from the musora-platform-backend Sanity schema into `src/lib/sanity/types/marketing.d.ts`. Generated, not hand-maintained, so schema changes are picked up by re-running the generator. Covered by `test/unit/generateSanityTypes.test.js`.
|
|
19
|
+
- `src/services/marketing/marketing.ts` exposes `fetchMarketingStats`, `fetchMarketingPracticeGoals`, `fetchMarketingTestimonials` and `fetchMarketingFaqs`, built on the newer `groq`/`query`/`filter` helpers in `src/lib/sanity/` rather than the legacy query strings in `sanity.js`.
|
|
20
|
+
|
|
21
|
+
All four documents are singletons per brand, so a single private `fetchBrandDocument<T>(type, brand)` helper applies `Filters.combine(Filters.type(type), Filters.brand(brand))`, takes `.first()`, and collapses the `Either` with `.recover(null)`. Each public function is a one-line wrapper that pins the generated type. `fetchMarketingFaqs` additionally takes `includeWebOnly = true`; when false it filters out questions flagged `web_only`, for mobile callers.
|
|
22
|
+
|
|
23
|
+
No field projections — the queries return the whole document, which is exactly what the generated types describe.
|
|
24
|
+
|
|
25
|
+
## Alternatives Considered
|
|
26
|
+
- One file per document type under `src/services/marketing/`. Rejected: four near-identical three-line functions, single file is smaller and easier to read.
|
|
27
|
+
- Explicit `select(...)` projections per document. Rejected: these documents are small and the generated type already covers every field; projections would need updating on every schema change for no measurable gain.
|
|
28
|
+
- Hand-written type declarations. Rejected: guaranteed to drift from the backend schema.
|
|
29
|
+
|
|
30
|
+
## Process Notes
|
|
31
|
+
- The generated `StatsDocument` is a discriminated union on `brand` — `musora` carries `total_student_count`, the instrument brands carry `learning_paths_count` and `artist_courses_count`. Callers must narrow on `brand` before reaching for those fields.
|
|
32
|
+
- `run<T>()` already returns `T | null` for a query that matched nothing; `.recover(null)` only handles the `Left` (query error) case.
|
|
33
|
+
- `src/index.js` / `src/index.d.ts` are generated by `npm run build-index` and were regenerated in this branch to export the four new functions.
|
|
34
|
+
|
|
35
|
+
## Consequences
|
|
36
|
+
- Marketing document access is typed and centralised; new marketing document types follow the same pattern.
|
|
37
|
+
- The generated types must be refreshed with `npm run gen-sanity-types` whenever the backend marketing schema changes — a stale `marketing.d.ts` will typecheck fine while being wrong.
|
|
38
|
+
- No new runtime dependencies.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
date: 2026-09-04
|
|
3
|
+
branch: refactor/sanity-client-cloudflare-cache
|
|
4
|
+
pr: https://github.com/railroadmedia/musora-content-services/pull/1046
|
|
5
|
+
status: open
|
|
6
|
+
tags: [[chore]]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Route SanityClient through Cloudflare caching proxy
|
|
10
|
+
|
|
11
|
+
## Context
|
|
12
|
+
|
|
13
|
+
PR #900 added a Cloudflare caching layer for Sanity queries by pointing the legacy `fetchSanity()` function (`src/services/sanity.js`) at `sanity.musora.com` instead of hitting `*.sanity.io` directly. That change only touched the legacy path. The newer `SanityClient` class (`src/infrastructure/sanity/SanityClient.ts`) builds its request URL independently in `FetchQueryExecutor.buildUrl()`, which still hit `*.sanity.io` directly and bypassed the CF cache entirely.
|
|
14
|
+
|
|
15
|
+
## Decision
|
|
16
|
+
|
|
17
|
+
Mirrored the same host rewrite from PR #900 into `FetchQueryExecutor.buildUrl()` (`src/infrastructure/sanity/executors/FetchQueryExecutor.ts`):
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
return `https://sanity.musora.com/${config.projectId}/${api}/v${config.version}/${config.dataset}?perspective=${perspective}`
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
No other files needed to change — `DefaultConfigProvider`, `SanityConfig`, and `SanityClient` itself are all host-agnostic and pass through unaffected.
|
|
24
|
+
|
|
25
|
+
## Alternatives Considered
|
|
26
|
+
|
|
27
|
+
No alternatives considered — this is a direct parity fix to bring `SanityClient` in line with the URL rewrite already proven in `fetchSanity()`.
|
|
28
|
+
|
|
29
|
+
## Process Notes
|
|
30
|
+
|
|
31
|
+
Updated `test/unit/infrastructure/sanity/FetchQueryExecutor.test.ts` — 4 assertions were hardcoded against the old `*.sanity.io` URL format and needed updating to the new `sanity.musora.com` host/path shape.
|
|
32
|
+
|
|
33
|
+
The diff also shows unrelated prettier-driven reformatting in `src/services/sanity.js` (line wrapping, trailing commas) picked up from a main sync — not part of this change's intent.
|
|
34
|
+
|
|
35
|
+
## Consequences
|
|
36
|
+
|
|
37
|
+
Both Sanity query paths (`fetchSanity()` and `SanityClient`) now route through the Cloudflare caching proxy, so queries made via `SanityClient` benefit from the same CDN caching as the legacy path.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(npx jest *)",
|
|
5
|
+
"Bash(npx tsc *)",
|
|
6
|
+
"Skill(counselors)",
|
|
7
|
+
"Bash(counselors ls *)",
|
|
8
|
+
"Bash(counselors groups *)",
|
|
9
|
+
"Bash(counselors run *)",
|
|
10
|
+
"Bash(npm test *)",
|
|
11
|
+
"Bash(gh pr *)",
|
|
12
|
+
"Bash(gh api *)",
|
|
13
|
+
"Bash(mkdir -p /tmp/pr-review-v2)",
|
|
14
|
+
"Read(//tmp/pr-review-v2/**)",
|
|
15
|
+
"Bash(cat /home/alesevero/railenvironment/applications/musora-content-services/AGENTS.md)",
|
|
16
|
+
"Bash(echo \"no AGENTS.md\")",
|
|
17
|
+
"Bash(echo \"exit=$?\")",
|
|
18
|
+
"Bash(git checkout *)",
|
|
19
|
+
"Skill(pr)",
|
|
20
|
+
"Skill(create-decision)",
|
|
21
|
+
"mcp__github__pull_request_read",
|
|
22
|
+
"mcp__github__push_files",
|
|
23
|
+
"Bash(git push *)",
|
|
24
|
+
"mcp__github__create_pull_request",
|
|
25
|
+
"mcp__github__search_pull_requests",
|
|
26
|
+
"Bash(git commit *)",
|
|
27
|
+
"Bash(git add *)",
|
|
28
|
+
"mcp__github__list_pull_requests",
|
|
29
|
+
"mcp__sanity__get_schema",
|
|
30
|
+
"mcp__sanity__query_documents",
|
|
31
|
+
"Bash(git rm *)",
|
|
32
|
+
"Bash(git rebase *)",
|
|
33
|
+
"Bash(git fetch *)",
|
|
34
|
+
"Bash(git --no-pager log --oneline -1)",
|
|
35
|
+
"Bash(git --no-pager log --oneline -5)",
|
|
36
|
+
"Bash(git ls-tree *)",
|
|
37
|
+
"Bash(git --no-pager log --oneline -2)",
|
|
38
|
+
"Bash(git --no-pager diff --stat HEAD~1)",
|
|
39
|
+
"Bash(git check-ignore *)",
|
|
40
|
+
"Bash(python3 -)",
|
|
41
|
+
"Bash(git --no-pager log --oneline -3)",
|
|
42
|
+
"Bash(mkdir *)",
|
|
43
|
+
"Bash(git --no-pager log --oneline origin/main..HEAD)"
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
"model": "sonnet",
|
|
47
|
+
"enabledMcpjsonServers": [
|
|
48
|
+
"atlassian",
|
|
49
|
+
"figma",
|
|
50
|
+
"google-workspace",
|
|
51
|
+
"snowflake",
|
|
52
|
+
"aws",
|
|
53
|
+
"hex",
|
|
54
|
+
"sanity",
|
|
55
|
+
"mysql",
|
|
56
|
+
"slack",
|
|
57
|
+
"langfuse",
|
|
58
|
+
"chrome-devtools",
|
|
59
|
+
"railway",
|
|
60
|
+
"github",
|
|
61
|
+
"asana"
|
|
62
|
+
],
|
|
63
|
+
"disabledMcpjsonServers": [
|
|
64
|
+
"nightwatch",
|
|
65
|
+
"1password",
|
|
66
|
+
"mysql-staging",
|
|
67
|
+
"mysql-local",
|
|
68
|
+
"mobile"
|
|
69
|
+
],
|
|
70
|
+
"effortLevel": "medium"
|
|
71
|
+
}
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
## [2.180.0](https://github.com/railroadmedia/musora-content-services/compare/v2.179.0...v2.180.0) (2026-09-04)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **TP-1335:** consolidate duplicated user data fetches ([#1039](https://github.com/railroadmedia/musora-content-services/issues/1039)) ([bb2af2c](https://github.com/railroadmedia/musora-content-services/commit/bb2af2cddce8da519a414b24d7b86cc96639faaf))
|
|
11
|
+
* **TP-1354:** daily session / active path get optimisations ([#1041](https://github.com/railroadmedia/musora-content-services/issues/1041)) ([bca652c](https://github.com/railroadmedia/musora-content-services/commit/bca652ccf181b1ba943fe6e9b14b375bf4bebfc9))
|
|
12
|
+
|
|
13
|
+
## [2.179.0](https://github.com/railroadmedia/musora-content-services/compare/v2.178.1...v2.179.0) (2026-09-03)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* **MU2-1877:** Add Sanity type generation and marketing query functions ([#1040](https://github.com/railroadmedia/musora-content-services/issues/1040)) ([a232299](https://github.com/railroadmedia/musora-content-services/commit/a232299286ce22fa04591a812c65039544682e37))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Bug Fixes
|
|
22
|
+
|
|
23
|
+
* **BR-725:** fetch tab data with progress applied ([#1044](https://github.com/railroadmedia/musora-content-services/issues/1044)) ([d19baf1](https://github.com/railroadmedia/musora-content-services/commit/d19baf1565d54a5e6179b6ae21b184ceb64c8aee))
|
|
24
|
+
|
|
5
25
|
### [2.178.1](https://github.com/railroadmedia/musora-content-services/compare/v2.178.0...v2.178.1) (2026-08-31)
|
|
6
26
|
|
|
7
27
|
## [2.178.0](https://github.com/railroadmedia/musora-content-services/compare/v2.177.1...v2.178.0) (2026-08-28)
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "musora-content-services",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.180.0",
|
|
4
4
|
"description": "A package for Musoras content services ",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"prepare": "node scripts/gen-version.cjs",
|
|
9
9
|
"build-index": "node tools/generate-index.cjs",
|
|
10
|
+
"gen-sanity-types": "node tools/generate-sanity-types.cjs",
|
|
10
11
|
"release": "standard-version",
|
|
11
12
|
"doc": "jsdoc -c jsdoc.json --verbose",
|
|
12
13
|
"test": "jest",
|
package/src/index.d.ts
CHANGED
|
@@ -224,6 +224,14 @@ import {
|
|
|
224
224
|
createTestUser
|
|
225
225
|
} from './services/liveTesting.ts';
|
|
226
226
|
|
|
227
|
+
import {
|
|
228
|
+
fetchMarketingAll,
|
|
229
|
+
fetchMarketingFaqs,
|
|
230
|
+
fetchMarketingPracticeGoals,
|
|
231
|
+
fetchMarketingStats,
|
|
232
|
+
fetchMarketingTestimonials
|
|
233
|
+
} from './services/marketing/marketing.ts';
|
|
234
|
+
|
|
227
235
|
import {
|
|
228
236
|
acceptInvite,
|
|
229
237
|
createAccount,
|
|
@@ -654,6 +662,11 @@ declare module 'musora-content-services' {
|
|
|
654
662
|
fetchLiveEvent,
|
|
655
663
|
fetchLiveEventPollingState,
|
|
656
664
|
fetchLiveStreamData,
|
|
665
|
+
fetchMarketingAll,
|
|
666
|
+
fetchMarketingFaqs,
|
|
667
|
+
fetchMarketingPracticeGoals,
|
|
668
|
+
fetchMarketingStats,
|
|
669
|
+
fetchMarketingTestimonials,
|
|
657
670
|
fetchMemberships,
|
|
658
671
|
fetchMetadata,
|
|
659
672
|
fetchMethodV2IntroVideo,
|
package/src/index.js
CHANGED
|
@@ -228,6 +228,14 @@ import {
|
|
|
228
228
|
createTestUser
|
|
229
229
|
} from './services/liveTesting.ts';
|
|
230
230
|
|
|
231
|
+
import {
|
|
232
|
+
fetchMarketingAll,
|
|
233
|
+
fetchMarketingFaqs,
|
|
234
|
+
fetchMarketingPracticeGoals,
|
|
235
|
+
fetchMarketingStats,
|
|
236
|
+
fetchMarketingTestimonials
|
|
237
|
+
} from './services/marketing/marketing.ts';
|
|
238
|
+
|
|
231
239
|
import {
|
|
232
240
|
acceptInvite,
|
|
233
241
|
createAccount,
|
|
@@ -653,6 +661,11 @@ export {
|
|
|
653
661
|
fetchLiveEvent,
|
|
654
662
|
fetchLiveEventPollingState,
|
|
655
663
|
fetchLiveStreamData,
|
|
664
|
+
fetchMarketingAll,
|
|
665
|
+
fetchMarketingFaqs,
|
|
666
|
+
fetchMarketingPracticeGoals,
|
|
667
|
+
fetchMarketingStats,
|
|
668
|
+
fetchMarketingTestimonials,
|
|
656
669
|
fetchMemberships,
|
|
657
670
|
fetchMetadata,
|
|
658
671
|
fetchMethodV2IntroVideo,
|
|
@@ -18,6 +18,7 @@ export class HttpClient {
|
|
|
18
18
|
private headerProvider: HeaderProvider
|
|
19
19
|
private requestExecutor: RequestExecutor
|
|
20
20
|
private credentials: RequestCredentials
|
|
21
|
+
private inFlightGetRequests: Map<string, Promise<any>> = new Map()
|
|
21
22
|
|
|
22
23
|
constructor(
|
|
23
24
|
baseUrl: string = '',
|
|
@@ -42,7 +43,20 @@ export class HttpClient {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
public async get<T>(url: string, options: _RequestOptions = {}): Promise<T> {
|
|
45
|
-
|
|
46
|
+
const key = `GET:${this.resolveUrl(url)}:${options.dataVersion || ''}`
|
|
47
|
+
|
|
48
|
+
const inFlightRequest = this.inFlightGetRequests.get(key)
|
|
49
|
+
if (inFlightRequest) {
|
|
50
|
+
return inFlightRequest
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const request = this.request<T>(url, 'GET', options).finally(() => {
|
|
54
|
+
this.inFlightGetRequests.delete(key)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
this.inFlightGetRequests.set(key, request)
|
|
58
|
+
|
|
59
|
+
return request
|
|
46
60
|
}
|
|
47
61
|
|
|
48
62
|
public async post<T>(url: string, data: any, options: _RequestOptions = {}): Promise<T> {
|
|
@@ -46,7 +46,7 @@ export class FetchQueryExecutor implements QueryExecutor {
|
|
|
46
46
|
private buildUrl(config: SanityConfig): string {
|
|
47
47
|
const perspective = config.perspective ?? 'published'
|
|
48
48
|
const api = config.useCachedAPI ? 'apicdn' : 'api'
|
|
49
|
-
return `https
|
|
49
|
+
return `https://sanity.musora.com/${config.projectId}/${api}/v${config.version}/${config.dataset}?perspective=${perspective}`
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
private buildRequest(
|
package/src/lib/ads/coproduct.ts
CHANGED
|
@@ -4,7 +4,14 @@ import { Recoverable } from './interfaces/recoverable'
|
|
|
4
4
|
import { Tappable } from './interfaces/tappable'
|
|
5
5
|
import { Monad } from './monad'
|
|
6
6
|
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* A monadic container holding either a left value (`L`, usually an error/failure) or a right
|
|
9
|
+
* value (`R`, usually a success). Convention: right is the "happy path" that `map`/`flatMap`
|
|
10
|
+
* operate on; left short-circuits those and passes through unchanged.
|
|
11
|
+
* @example
|
|
12
|
+
* Coproduct.right(5).map(n => n + 1) // Right(6)
|
|
13
|
+
* Coproduct.left('err').map((n: number) => n) // Left('err'), fn never called
|
|
14
|
+
*/
|
|
8
15
|
export abstract class Coproduct<L, R>
|
|
9
16
|
implements
|
|
10
17
|
Tappable<L | R>,
|
|
@@ -13,40 +20,71 @@ export abstract class Coproduct<L, R>
|
|
|
13
20
|
Recoverable<R>,
|
|
14
21
|
Monad<L | R>
|
|
15
22
|
{
|
|
23
|
+
/** @param value - the left value to wrap */
|
|
16
24
|
static left<L, R>(value: L): Coproduct<L, R> {
|
|
17
25
|
return new Left(value)
|
|
18
26
|
}
|
|
19
27
|
|
|
28
|
+
/** @param value - the right value to wrap */
|
|
20
29
|
static right<L, R>(value: R): Coproduct<L, R> {
|
|
21
30
|
return new Right(value)
|
|
22
31
|
}
|
|
23
32
|
|
|
33
|
+
/** Type guard: true if this is a {@link Left}. */
|
|
24
34
|
abstract isLeft(): this is Left<L, R>
|
|
35
|
+
/** Type guard: true if this is a {@link Right}. */
|
|
25
36
|
abstract isRight(): this is Right<L, R>
|
|
26
37
|
|
|
27
38
|
/**
|
|
28
39
|
* @extends Functor
|
|
29
40
|
* Maps the right value of the Coproduct.
|
|
41
|
+
* @example
|
|
42
|
+
* Coproduct.right(2).map(n => n * 2) // Right(4)
|
|
43
|
+
* Coproduct.left('err').map(n => n) // Left('err'), fn never called
|
|
30
44
|
*/
|
|
31
45
|
abstract map<T>(fn: (r: R) => T): Coproduct<L, T>
|
|
46
|
+
|
|
32
47
|
/**
|
|
33
48
|
* Maps the right value of the Coproduct with an asynchronous function.
|
|
34
49
|
* A rejection propagates: there is no way to build an L from it.
|
|
50
|
+
* @example
|
|
51
|
+
* await Coproduct.right(userId).mapAsync(id => fetchUser(id)) // Right(user)
|
|
35
52
|
*/
|
|
36
53
|
abstract mapAsync<T>(fn: (r: R) => Promise<T>): Promise<Coproduct<L, T>>
|
|
54
|
+
|
|
37
55
|
/**
|
|
38
56
|
* @extends Monad
|
|
39
|
-
*
|
|
57
|
+
* Like {@link map}, but `fn` itself returns a Coproduct instead of a plain value, so nested
|
|
58
|
+
* Coproducts get flattened. No-op on Left.
|
|
59
|
+
* @example
|
|
60
|
+
* Coproduct.right(id).flatMap(id => validate(id)) // validate returns a Coproduct itself
|
|
40
61
|
*/
|
|
41
62
|
abstract flatMap<T>(fn: (r: R) => Coproduct<L, T>): Coproduct<L, T>
|
|
42
63
|
|
|
43
|
-
/**
|
|
44
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Maps the left value of the Coproduct to a new type. No-op on Right.
|
|
66
|
+
* @example
|
|
67
|
+
* Coproduct.left(new Error('bad')).lmap(e => e.message) // Left('bad')
|
|
68
|
+
*/
|
|
69
|
+
abstract lmap<T>(fn: (l: L) => T): Coproduct<T, R>
|
|
45
70
|
|
|
46
|
-
/**
|
|
47
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Left-side equivalent of {@link flatMap}: `fn` returns a Coproduct, flattening nested
|
|
73
|
+
* results. No-op on Right.
|
|
74
|
+
*/
|
|
75
|
+
abstract lflatMap<T>(fn: (l: L) => Coproduct<T, R>): Coproduct<T, R>
|
|
48
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Unwraps the Coproduct by calling whichever handler matches its side.
|
|
79
|
+
* @example
|
|
80
|
+
* result.fold(
|
|
81
|
+
* err => showError(err),
|
|
82
|
+
* value => showValue(value)
|
|
83
|
+
* )
|
|
84
|
+
*/
|
|
49
85
|
abstract fold<T, U>(onLeft: (l: L) => T, onRight: (r: R) => U): T | U
|
|
86
|
+
|
|
87
|
+
/** Convenience {@link fold} that applies the same `fn` regardless of side. */
|
|
50
88
|
foldMap<T>(initial: T, fn: (acc: T, value: L | R) => T): T {
|
|
51
89
|
return this.fold(
|
|
52
90
|
(l) => fn(initial, l),
|
|
@@ -57,19 +95,28 @@ export abstract class Coproduct<L, R>
|
|
|
57
95
|
/**
|
|
58
96
|
* Visits the value inside the container if right and applies a function to it without modifying
|
|
59
97
|
* @implements Tappable
|
|
98
|
+
* @example
|
|
99
|
+
* Coproduct.right(user).tap(u => console.log('loaded', u.id)) // logs, still Right(user)
|
|
60
100
|
*/
|
|
61
101
|
abstract tap(fn: (r: R) => void): this
|
|
62
102
|
|
|
63
103
|
/** Visits the value inside the container if left and applies a function to it without modifying */
|
|
64
104
|
abstract ltap(fn: (l: L) => void): Coproduct<L, R>
|
|
105
|
+
|
|
106
|
+
/** Unwraps the Coproduct, returning whichever value it holds (left or right) untouched. */
|
|
65
107
|
abstract drop(): R | L
|
|
108
|
+
|
|
66
109
|
/**
|
|
67
110
|
* Returns the right value if it exists, otherwise returns the provided default value.
|
|
68
111
|
* @implements Recoverable
|
|
112
|
+
* @example
|
|
113
|
+
* Coproduct.left('err').recover(0) // 0
|
|
114
|
+
* Coproduct.right(5).recover(0) // 5
|
|
69
115
|
*/
|
|
70
116
|
abstract recover(defaultValue: R): R
|
|
71
117
|
}
|
|
72
118
|
|
|
119
|
+
/** A left value in a Coproduct, usually representing a failure or invalid result. */
|
|
73
120
|
export class Left<L, R> extends Coproduct<L, R> {
|
|
74
121
|
constructor(private readonly value: L) {
|
|
75
122
|
super()
|
|
@@ -95,11 +142,11 @@ export class Left<L, R> extends Coproduct<L, R> {
|
|
|
95
142
|
return new Left(this.value)
|
|
96
143
|
}
|
|
97
144
|
|
|
98
|
-
|
|
145
|
+
lmap<T>(fn: (l: L) => T): Coproduct<T, R> {
|
|
99
146
|
return new Left(fn(this.value))
|
|
100
147
|
}
|
|
101
148
|
|
|
102
|
-
|
|
149
|
+
lflatMap<T>(fn: (l: L) => Coproduct<T, R>): Coproduct<T, R> {
|
|
103
150
|
return fn(this.value)
|
|
104
151
|
}
|
|
105
152
|
|
|
@@ -152,11 +199,11 @@ export class Right<L, R> extends Coproduct<L, R> {
|
|
|
152
199
|
return fn(this.value)
|
|
153
200
|
}
|
|
154
201
|
|
|
155
|
-
|
|
202
|
+
lmap<T>(_fn: (l: L) => T): Coproduct<T, R> {
|
|
156
203
|
return new Right(this.value)
|
|
157
204
|
}
|
|
158
205
|
|
|
159
|
-
|
|
206
|
+
lflatMap<T>(_fn: (l: L) => Coproduct<T, R>): Coproduct<T, R> {
|
|
160
207
|
return new Right(this.value)
|
|
161
208
|
}
|
|
162
209
|
|
package/src/lib/ads/either.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { Coproduct } from './coproduct'
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* A {@link Coproduct} under the convention that left carries the error and right carries the
|
|
5
|
+
* success value.
|
|
6
|
+
* @example
|
|
7
|
+
* function parseAge(input: string): Either<string, number> {
|
|
8
|
+
* const n = Number(input)
|
|
9
|
+
* return Number.isNaN(n) ? Either.left('not a number') : Either.right(n)
|
|
10
|
+
* }
|
|
11
|
+
* parseAge('42').map(n => n + 1) // Right(43)
|
|
12
|
+
* parseAge('x').map(n => n + 1) // Left('not a number')
|
|
13
|
+
*/
|
|
4
14
|
export type Either<L, R> = Coproduct<L, R>
|
|
5
15
|
|
|
6
16
|
export const Either = {
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Generated by tools/generate-sanity-types.cjs from the musora-platform-backend Sanity schema.
|
|
2
|
+
// Do not edit by hand; re-run the generator instead.
|
|
3
|
+
export type StatsDocument = {
|
|
4
|
+
_id: string
|
|
5
|
+
_type: "stats"
|
|
6
|
+
_createdAt: string
|
|
7
|
+
_updatedAt: string
|
|
8
|
+
_rev: string
|
|
9
|
+
/** Songs Count */
|
|
10
|
+
songs_count?: string
|
|
11
|
+
} & (
|
|
12
|
+
| {
|
|
13
|
+
brand: "musora"
|
|
14
|
+
/** Total Student Count */
|
|
15
|
+
total_student_count?: string
|
|
16
|
+
}
|
|
17
|
+
| {
|
|
18
|
+
brand: "drumeo" | "pianote" | "singeo" | "guitareo" | "playbass"
|
|
19
|
+
/** Learning Paths Count */
|
|
20
|
+
learning_paths_count?: string
|
|
21
|
+
/** Artist Courses Count */
|
|
22
|
+
artist_courses_count?: string
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
export type PracticeGoalDocument = {
|
|
27
|
+
_id: string
|
|
28
|
+
_type: "practice-goal"
|
|
29
|
+
_createdAt: string
|
|
30
|
+
_updatedAt: string
|
|
31
|
+
_rev: string
|
|
32
|
+
brand: "drumeo" | "pianote" | "singeo" | "guitareo" | "playbass"
|
|
33
|
+
/** <2 Days a Week */
|
|
34
|
+
under_two_days_a_week?: {
|
|
35
|
+
/** First Day */
|
|
36
|
+
first_day?: string
|
|
37
|
+
/** First Week */
|
|
38
|
+
first_week?: string
|
|
39
|
+
/** First Month */
|
|
40
|
+
first_month?: string
|
|
41
|
+
}
|
|
42
|
+
/** 3-4 Days a Week */
|
|
43
|
+
three_to_four_days_a_week?: {
|
|
44
|
+
/** First Day */
|
|
45
|
+
first_day?: string
|
|
46
|
+
/** First Week */
|
|
47
|
+
first_week?: string
|
|
48
|
+
/** First Month */
|
|
49
|
+
first_month?: string
|
|
50
|
+
}
|
|
51
|
+
/** 5+ Days a Week */
|
|
52
|
+
five_plus_days_a_week?: {
|
|
53
|
+
/** First Day */
|
|
54
|
+
first_day?: string
|
|
55
|
+
/** First Week */
|
|
56
|
+
first_week?: string
|
|
57
|
+
/** First Month */
|
|
58
|
+
first_month?: string
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type TestimonialDocument = {
|
|
63
|
+
_id: string
|
|
64
|
+
_type: "testimonial"
|
|
65
|
+
_createdAt: string
|
|
66
|
+
_updatedAt: string
|
|
67
|
+
_rev: string
|
|
68
|
+
brand: "musora" | "drumeo" | "pianote" | "singeo" | "guitareo" | "playbass"
|
|
69
|
+
/** Testimonials */
|
|
70
|
+
testimonials?: {
|
|
71
|
+
_key: string
|
|
72
|
+
name?: string
|
|
73
|
+
location?: string
|
|
74
|
+
/** Testimonial Copy */
|
|
75
|
+
copy?: string
|
|
76
|
+
}[]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type FaqDocument = {
|
|
80
|
+
_id: string
|
|
81
|
+
_type: "faq"
|
|
82
|
+
_createdAt: string
|
|
83
|
+
_updatedAt: string
|
|
84
|
+
_rev: string
|
|
85
|
+
brand: "musora" | "drumeo" | "pianote" | "singeo" | "guitareo" | "playbass"
|
|
86
|
+
/** FAQs */
|
|
87
|
+
questions?: {
|
|
88
|
+
_key: string
|
|
89
|
+
/** Web Only? */
|
|
90
|
+
web_only?: boolean
|
|
91
|
+
question?: string
|
|
92
|
+
answer?: string
|
|
93
|
+
}[]
|
|
94
|
+
}
|
package/src/services/config.js
CHANGED
|
@@ -9,6 +9,7 @@ export let globalConfig = {
|
|
|
9
9
|
sanityConfig: {},
|
|
10
10
|
railcontentConfig: {},
|
|
11
11
|
sessionConfig: {},
|
|
12
|
+
sessionUser: null,
|
|
12
13
|
localStorage: null,
|
|
13
14
|
isMA: false,
|
|
14
15
|
localTimezoneString: null, // In format: America/Vancouver
|
|
@@ -109,3 +110,7 @@ export function initializeService(config) {
|
|
|
109
110
|
export function initializeEnvVar(config) {
|
|
110
111
|
globalConfig.appEnv = config.appEnv
|
|
111
112
|
}
|
|
113
|
+
|
|
114
|
+
export function setSessionUserData(userData) {
|
|
115
|
+
globalConfig.sessionUser = userData || null
|
|
116
|
+
}
|