musora-content-services 2.89.0 → 2.92.3
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/CHANGELOG.md +31 -0
- package/docs/ContentOrganization.html +2 -2
- package/docs/Forums.html +2 -2
- package/docs/Gamification.html +2 -2
- package/docs/TestUser.html +2 -2
- package/docs/UserManagementSystem.html +2 -2
- package/docs/api_types.js.html +2 -2
- package/docs/config.js.html +2 -2
- package/docs/content-org_content-org.js.html +2 -2
- package/docs/content-org_guided-courses.ts.html +2 -2
- package/docs/content-org_learning-paths.ts.html +52 -40
- package/docs/content-org_playlists-types.js.html +2 -2
- package/docs/content-org_playlists.js.html +2 -2
- package/docs/content.js.html +2 -2
- package/docs/content_artist.ts.html +2 -2
- package/docs/content_genre.ts.html +2 -2
- package/docs/content_instructor.ts.html +2 -2
- package/docs/forums_categories.ts.html +2 -2
- package/docs/forums_forums.ts.html +2 -2
- package/docs/forums_posts.ts.html +2 -2
- package/docs/forums_threads.ts.html +2 -2
- package/docs/gamification_awards.ts.html +2 -2
- package/docs/gamification_gamification.js.html +2 -2
- package/docs/global.html +2 -2
- package/docs/index.html +2 -2
- package/docs/liveTesting.ts.html +2 -2
- package/docs/module-Accounts.html +2 -2
- package/docs/module-Artist.html +2 -2
- package/docs/module-Awards.html +2 -2
- package/docs/module-Config.html +2 -2
- package/docs/module-Content-Services-V2.html +2 -2
- package/docs/module-Forums.html +2 -2
- package/docs/module-Genre.html +2 -2
- package/docs/module-GuidedCourses.html +2 -2
- package/docs/module-Instructor.html +2 -2
- package/docs/module-Interests.html +2 -2
- package/docs/module-LearningPaths.html +269 -143
- package/docs/module-Onboarding.html +3 -3
- package/docs/module-Payments.html +2 -2
- package/docs/module-Permissions.html +2 -2
- package/docs/module-Playlists.html +2 -2
- package/docs/module-ProgressRow.html +2 -2
- package/docs/module-Railcontent-Services.html +34 -893
- package/docs/module-Sanity-Services.html +2 -2
- package/docs/module-Sessions.html +2 -2
- package/docs/module-UserActivity.html +70 -116
- package/docs/module-UserChat.html +2 -2
- package/docs/module-UserManagement.html +2 -2
- package/docs/module-UserMemberships.html +2 -2
- package/docs/module-UserNotifications.html +2 -2
- package/docs/module-UserProfile.html +2 -2
- package/docs/progress-row_method-card.js.html +3 -2
- package/docs/railcontent.js.html +14 -137
- package/docs/sanity.js.html +2 -2
- package/docs/userActivity.js.html +85 -150
- package/docs/user_account.ts.html +2 -2
- package/docs/user_chat.js.html +2 -2
- package/docs/user_interests.js.html +2 -2
- package/docs/user_management.js.html +2 -2
- package/docs/user_memberships.ts.html +2 -2
- package/docs/user_notifications.js.html +2 -2
- package/docs/user_onboarding.ts.html +10 -6
- package/docs/user_payments.ts.html +2 -2
- package/docs/user_permissions.js.html +2 -2
- package/docs/user_profile.js.html +2 -2
- package/docs/user_sessions.js.html +2 -2
- package/docs/user_types.js.html +2 -2
- package/docs/user_user-management-system.js.html +2 -2
- package/package.json +11 -3
- package/src/contentTypeConfig.js +6 -0
- package/src/index.d.ts +7 -31
- package/src/index.js +10 -34
- package/src/services/content-org/learning-paths.ts +31 -0
- package/src/services/contentAggregator.js +2 -2
- package/src/services/contentLikes.js +6 -39
- package/src/services/contentProgress.js +181 -479
- package/src/services/dataContext.js +0 -2
- package/src/services/progress-row/method-card.js +1 -0
- package/src/services/railcontent.js +12 -135
- package/src/services/sentry/.indexignore +0 -0
- package/src/services/sentry/index.ts +23 -0
- package/src/services/sync/.indexignore +0 -0
- package/src/services/sync/adapters/factory.ts +26 -0
- package/src/services/sync/adapters/lokijs.ts +1 -0
- package/src/services/sync/adapters/sqlite.ts +1 -0
- package/src/services/sync/concurrency-safety.ts +4 -0
- package/src/services/sync/context/index.ts +43 -0
- package/src/services/sync/context/providers/base.ts +4 -0
- package/src/services/sync/context/providers/connectivity.ts +14 -0
- package/src/services/sync/context/providers/durability.ts +5 -0
- package/src/services/sync/context/providers/index.ts +5 -0
- package/src/services/sync/context/providers/session.ts +8 -0
- package/src/services/sync/context/providers/tabs.ts +18 -0
- package/src/services/sync/context/providers/visibility.ts +14 -0
- package/src/services/sync/database/factory.ts +10 -0
- package/src/services/sync/errors/boundary.ts +45 -0
- package/src/services/sync/errors/index.ts +49 -0
- package/src/services/sync/fetch.ts +310 -0
- package/src/services/sync/index.ts +80 -0
- package/src/services/sync/manager.ts +139 -0
- package/src/services/sync/models/Base.ts +47 -0
- package/src/services/sync/models/ContentLike.ts +16 -0
- package/src/services/sync/models/ContentProgress.ts +69 -0
- package/src/services/sync/models/Practice.ts +72 -0
- package/src/services/sync/models/PracticeDayNote.ts +23 -0
- package/src/services/sync/models/index.ts +4 -0
- package/src/services/sync/repositories/base.ts +247 -0
- package/src/services/sync/repositories/content-likes.ts +26 -0
- package/src/services/sync/repositories/content-progress.ts +160 -0
- package/src/services/sync/repositories/index.ts +4 -0
- package/src/services/sync/repositories/practice-day-notes.ts +4 -0
- package/src/services/sync/repositories/practices.ts +52 -0
- package/src/services/sync/repository-proxy.ts +48 -0
- package/src/services/sync/resolver.ts +84 -0
- package/src/services/sync/retry.ts +88 -0
- package/src/services/sync/run-scope.ts +30 -0
- package/src/services/sync/schema/index.ts +66 -0
- package/src/services/sync/serializers/index.ts +2 -0
- package/src/services/sync/serializers/model.ts +32 -0
- package/src/services/sync/serializers/raw.ts +21 -0
- package/src/services/sync/store/index.ts +779 -0
- package/src/services/sync/store/push-coalescer.ts +57 -0
- package/src/services/sync/store-configs.ts +41 -0
- package/src/services/sync/strategies/base.ts +21 -0
- package/src/services/sync/strategies/index.ts +12 -0
- package/src/services/sync/strategies/initial.ts +11 -0
- package/src/services/sync/strategies/polling.ts +54 -0
- package/src/services/sync/telemetry/index.ts +140 -0
- package/src/services/sync/telemetry/sampling.ts +91 -0
- package/src/services/sync/utils/event-emitter.ts +24 -0
- package/src/services/sync/utils/index.ts +1 -0
- package/src/services/sync/utils/throttle.ts +93 -0
- package/src/services/sync/utils/timers.ts +9 -0
- package/src/services/userActivity.js +83 -148
- package/test/contentProgress.test.js +6 -39
- package/test/live/contentProgressLive.test.js +2 -31
- package/tools/generate-index.cjs +10 -4
- package/.claude/settings.local.json +0 -8
- package/babel.config.cjs +0 -3
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { RecordId } from "@nozbe/watermelondb";
|
|
2
|
+
import { SyncEntry, SyncEntryNonDeleted } from ".";
|
|
3
|
+
import BaseModel from "./models/Base";
|
|
4
|
+
|
|
5
|
+
export type SyncResolution = {
|
|
6
|
+
entriesForCreate: SyncEntry[]
|
|
7
|
+
tuplesForUpdate: [BaseModel, SyncEntry][]
|
|
8
|
+
tuplesForRestore: [BaseModel, SyncEntry][]
|
|
9
|
+
idsForDestroy: RecordId[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type SyncResolverComparator<T extends BaseModel = BaseModel> = (serverEntry: SyncEntryNonDeleted<T>, localModel: T) => 'SERVER' | 'LOCAL'
|
|
13
|
+
|
|
14
|
+
export const updatedAtComparator: SyncResolverComparator = (server, local) => {
|
|
15
|
+
return server.meta.lifecycle.updated_at >= local.updated_at ? 'SERVER' : 'LOCAL'
|
|
16
|
+
}
|
|
17
|
+
export default class SyncResolver {
|
|
18
|
+
private resolution: SyncResolution
|
|
19
|
+
private comparator: SyncResolverComparator
|
|
20
|
+
|
|
21
|
+
constructor(comparator?: SyncResolverComparator) {
|
|
22
|
+
this.comparator = comparator || updatedAtComparator
|
|
23
|
+
this.resolution = {
|
|
24
|
+
entriesForCreate: [],
|
|
25
|
+
tuplesForUpdate: [],
|
|
26
|
+
tuplesForRestore: [],
|
|
27
|
+
idsForDestroy: []
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get result() {
|
|
32
|
+
return { ...this.resolution }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
againstNone(server: SyncEntry) {
|
|
36
|
+
if (!server.meta.lifecycle.deleted_at) {
|
|
37
|
+
this.resolution.entriesForCreate.push(server)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
againstSynced(local: BaseModel, server: SyncEntry) {
|
|
42
|
+
if (server.meta.lifecycle.deleted_at) {
|
|
43
|
+
this.resolution.idsForDestroy.push(local.id)
|
|
44
|
+
}
|
|
45
|
+
// take care that the server stamp isn't older than the current local
|
|
46
|
+
// (imagine a race condition where a pull request resolves long after a second one)
|
|
47
|
+
else if (this.comparator(server as SyncEntryNonDeleted<BaseModel>, local) !== 'LOCAL') {
|
|
48
|
+
this.resolution.tuplesForUpdate.push([local, server])
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// can happen if one tab notifies another of a created record, pushes to server, and other tab pulls
|
|
53
|
+
againstCreated(local: BaseModel, server: SyncEntry) {
|
|
54
|
+
if (server.meta.lifecycle.deleted_at) {
|
|
55
|
+
// delete local even though user has newer changes
|
|
56
|
+
// (we don't ever try to resurrect records here)
|
|
57
|
+
this.resolution.idsForDestroy.push(local.id)
|
|
58
|
+
} else if (this.comparator(server as SyncEntryNonDeleted<BaseModel>, local) !== 'LOCAL') {
|
|
59
|
+
// local is older, so update it with server's
|
|
60
|
+
this.resolution.tuplesForUpdate.push([local, server])
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
againstUpdated(local: BaseModel, server: SyncEntry) {
|
|
65
|
+
if (server.meta.lifecycle.deleted_at) {
|
|
66
|
+
// delete local even though user has newer changes
|
|
67
|
+
// (we don't ever try to resurrect records here)
|
|
68
|
+
this.resolution.idsForDestroy.push(local.id);
|
|
69
|
+
} else if (this.comparator(server as SyncEntryNonDeleted<BaseModel>, local) !== 'LOCAL') {
|
|
70
|
+
// local is older, so update it with server's
|
|
71
|
+
this.resolution.tuplesForUpdate.push([local, server])
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
againstDeleted(local: BaseModel, server: SyncEntry) {
|
|
76
|
+
if (server.meta.lifecycle.deleted_at) {
|
|
77
|
+
this.resolution.idsForDestroy.push(local.id)
|
|
78
|
+
} else if (server.meta.lifecycle.updated_at >= local.updated_at) {
|
|
79
|
+
this.resolution.tuplesForRestore.push([local, server])
|
|
80
|
+
} else {
|
|
81
|
+
this.resolution.idsForDestroy.push(local.id);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import SyncContext from "./context"
|
|
2
|
+
import { SyncResponse } from "./fetch"
|
|
3
|
+
import { SyncTelemetry, Span, StartSpanOptions } from "./telemetry/index"
|
|
4
|
+
|
|
5
|
+
export default class SyncRetry {
|
|
6
|
+
private readonly BASE_BACKOFF = 1_000
|
|
7
|
+
private readonly MAX_BACKOFF = 8_000
|
|
8
|
+
private readonly MAX_ATTEMPTS = 4
|
|
9
|
+
|
|
10
|
+
private paused = false
|
|
11
|
+
private backoffUntil = 0
|
|
12
|
+
private failureCount = 0
|
|
13
|
+
|
|
14
|
+
private unsubscribeConnectivity: () => void
|
|
15
|
+
|
|
16
|
+
constructor(private readonly context: SyncContext, private readonly telemetry: SyncTelemetry) {}
|
|
17
|
+
|
|
18
|
+
start() {
|
|
19
|
+
this.unsubscribeConnectivity = this.context.connectivity.subscribe(isOnline => {
|
|
20
|
+
if (isOnline && this.paused) {
|
|
21
|
+
this.paused = false
|
|
22
|
+
this.resetBackoff()
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
stop() {
|
|
28
|
+
this.unsubscribeConnectivity?.()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Runs the given syncFn with automatic retries.
|
|
33
|
+
* Returns the first successful result or the last failed result after retries.
|
|
34
|
+
*/
|
|
35
|
+
async request<T extends SyncResponse>(spanOpts: StartSpanOptions, syncFn: (span: Span) => Promise<T>) {
|
|
36
|
+
let attempt = 0
|
|
37
|
+
|
|
38
|
+
while (true) {
|
|
39
|
+
if (!this.context.connectivity.getValue()) {
|
|
40
|
+
this.telemetry.debug('[Retry] No connectivity - skipping')
|
|
41
|
+
this.paused = true
|
|
42
|
+
return { ok: false } as T
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const now = Date.now()
|
|
46
|
+
if (now < this.backoffUntil) {
|
|
47
|
+
await this.sleep(this.backoffUntil - now)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
attempt++
|
|
51
|
+
|
|
52
|
+
const spanOptions = { ...spanOpts, name: `${spanOpts.name}:attempt:${attempt}/${this.MAX_ATTEMPTS}`, op: `${spanOpts.op}:attempt` }
|
|
53
|
+
const result = await this.telemetry.trace(spanOptions, span => syncFn(span))
|
|
54
|
+
|
|
55
|
+
if (result.ok) {
|
|
56
|
+
this.resetBackoff()
|
|
57
|
+
return result
|
|
58
|
+
} else {
|
|
59
|
+
this.scheduleBackoff()
|
|
60
|
+
if (attempt >= this.MAX_ATTEMPTS) return result
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private resetBackoff() {
|
|
66
|
+
if (this.backoffUntil !== 0 || this.failureCount !== 0) {
|
|
67
|
+
this.telemetry.debug('[Retry] Resetting backoff')
|
|
68
|
+
this.backoffUntil = 0
|
|
69
|
+
this.failureCount = 0
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private scheduleBackoff() {
|
|
74
|
+
this.failureCount++
|
|
75
|
+
|
|
76
|
+
const exponentialDelay = this.BASE_BACKOFF * Math.pow(2, this.failureCount - 1)
|
|
77
|
+
const jitter = exponentialDelay * 0.25 * (Math.random() - 0.5)
|
|
78
|
+
const delayWithJitter = exponentialDelay + jitter
|
|
79
|
+
|
|
80
|
+
this.backoffUntil = Date.now() + Math.min(this.MAX_BACKOFF, delayWithJitter)
|
|
81
|
+
|
|
82
|
+
this.telemetry.debug('[Retry] Scheduling backoff', { failureCount: this.failureCount, backoffUntil: this.backoffUntil })
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private sleep(ms: number) {
|
|
86
|
+
return new Promise(resolve => setTimeout(resolve, ms))
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export default class SyncRunScope {
|
|
2
|
+
private abortController: AbortController
|
|
3
|
+
|
|
4
|
+
constructor() {
|
|
5
|
+
this.abortController = new AbortController()
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
get signal(): AbortSignal {
|
|
9
|
+
return this.abortController.signal
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
abort(): void {
|
|
13
|
+
this.abortController.abort()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
abortable<T>(fn: () => Promise<T>): Promise<T> {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
if (this.signal.aborted) {
|
|
19
|
+
reject(this.signal.reason)
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
fn().then(resolve).catch(reject)
|
|
24
|
+
|
|
25
|
+
this.signal.addEventListener('abort', () => {
|
|
26
|
+
reject(this.signal.reason)
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { appSchema, tableSchema } from '@nozbe/watermelondb'
|
|
2
|
+
|
|
3
|
+
export const SYNC_TABLES = {
|
|
4
|
+
CONTENT_LIKES: 'content_likes',
|
|
5
|
+
CONTENT_PROGRESS: 'progress',
|
|
6
|
+
PRACTICES: 'practices',
|
|
7
|
+
PRACTICE_DAY_NOTES: 'practice_day_notes'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const contentLikesTable = tableSchema({
|
|
11
|
+
name: SYNC_TABLES.CONTENT_LIKES,
|
|
12
|
+
columns: [
|
|
13
|
+
{ name: 'content_id', type: 'number', isIndexed: true },
|
|
14
|
+
{ name: 'created_at', type: 'number' },
|
|
15
|
+
{ name: 'updated_at', type: 'number' }
|
|
16
|
+
]
|
|
17
|
+
})
|
|
18
|
+
const contentProgressTable = tableSchema({
|
|
19
|
+
name: SYNC_TABLES.CONTENT_PROGRESS,
|
|
20
|
+
columns: [
|
|
21
|
+
{ name: 'content_id', type: 'number', isIndexed: true },
|
|
22
|
+
{ name: 'content_brand', type: 'string', isIndexed: true },
|
|
23
|
+
{ name: 'collection_type', type: 'string', isOptional: true, isIndexed: true },
|
|
24
|
+
{ name: 'collection_id', type: 'number', isOptional: true, isIndexed: true },
|
|
25
|
+
{ name: 'state', type: 'string', isIndexed: true },
|
|
26
|
+
{ name: 'progress_percent', type: 'number' },
|
|
27
|
+
{ name: 'resume_time_seconds', type: 'number' },
|
|
28
|
+
{ name: 'created_at', type: 'number' },
|
|
29
|
+
{ name: 'updated_at', type: 'number', isIndexed: true }
|
|
30
|
+
]
|
|
31
|
+
})
|
|
32
|
+
const practicesTable = tableSchema({
|
|
33
|
+
name: SYNC_TABLES.PRACTICES,
|
|
34
|
+
columns: [
|
|
35
|
+
{ name: 'manual_id', type: 'string', isOptional: true },
|
|
36
|
+
{ name: 'content_id', type: 'number', isOptional: true, isIndexed: true },
|
|
37
|
+
{ name: 'date', type: 'string', isIndexed: true },
|
|
38
|
+
{ name: 'auto', type: 'boolean', isIndexed: true },
|
|
39
|
+
{ name: 'duration_seconds', type: 'number' },
|
|
40
|
+
{ name: 'title', type: 'string', isOptional: true },
|
|
41
|
+
{ name: 'thumbnail_url', type: 'string', isOptional: true },
|
|
42
|
+
{ name: 'category_id', type: 'number', isOptional: true },
|
|
43
|
+
{ name: 'instrument_id', type: 'number', isOptional: true },
|
|
44
|
+
{ name: 'created_at', type: 'number' },
|
|
45
|
+
{ name: 'updated_at', type: 'number', isIndexed: true }
|
|
46
|
+
]
|
|
47
|
+
})
|
|
48
|
+
const practiceDayNotesTable = tableSchema({
|
|
49
|
+
name: SYNC_TABLES.PRACTICE_DAY_NOTES,
|
|
50
|
+
columns: [
|
|
51
|
+
{ name: 'date', type: 'string', isIndexed: true },
|
|
52
|
+
{ name: 'notes', type: 'string' },
|
|
53
|
+
{ name: 'created_at', type: 'number' },
|
|
54
|
+
{ name: 'updated_at', type: 'number', isIndexed: true }
|
|
55
|
+
]
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
export default appSchema({
|
|
59
|
+
version: 1,
|
|
60
|
+
tables: [
|
|
61
|
+
contentLikesTable,
|
|
62
|
+
contentProgressTable,
|
|
63
|
+
practicesTable,
|
|
64
|
+
practiceDayNotesTable
|
|
65
|
+
]
|
|
66
|
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Model, RecordId } from "@nozbe/watermelondb"
|
|
2
|
+
import BaseModel from "../models/Base"
|
|
3
|
+
|
|
4
|
+
export type ModelSerialized<TModel extends Model> = ExtractGetters<TModel>
|
|
5
|
+
type ExtractGetters<T> = {
|
|
6
|
+
[K in keyof T as T[K] extends Function ? never : K]: T[K];
|
|
7
|
+
} & { id: RecordId }
|
|
8
|
+
|
|
9
|
+
// serializes a record to a POJO based on its model getters
|
|
10
|
+
// (essentially strips out all watermelon properties)
|
|
11
|
+
// useful for consumption in components, etc.
|
|
12
|
+
|
|
13
|
+
export default class ModelSerializer<TModel extends Model = Model> {
|
|
14
|
+
toPlainObject(record: TModel) {
|
|
15
|
+
const proto = Object.getPrototypeOf(record)
|
|
16
|
+
const keys = [
|
|
17
|
+
...Object.getOwnPropertyNames(proto),
|
|
18
|
+
...Object.getOwnPropertyNames(BaseModel.prototype)
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
const result = {}
|
|
22
|
+
for (const key of keys) {
|
|
23
|
+
const desc = Object.getOwnPropertyDescriptor(proto, key) || Object.getOwnPropertyDescriptor(BaseModel.prototype, key)
|
|
24
|
+
if (desc?.get) {
|
|
25
|
+
result[key] = desc.get.call(record)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
result['id'] = record.id
|
|
29
|
+
|
|
30
|
+
return result as ModelSerialized<TModel>
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Model } from "@nozbe/watermelondb"
|
|
2
|
+
|
|
3
|
+
export type RawSerialized<TModel extends Model> = Omit<TModel['_raw'], '_changed' | '_status'>
|
|
4
|
+
|
|
5
|
+
// serializes a record to a POJO based on its _raw attributes
|
|
6
|
+
// useful for sending to back-end for sync
|
|
7
|
+
|
|
8
|
+
export default class RawSerializer<TModel extends Model = Model> {
|
|
9
|
+
toPlainObject(model: TModel) {
|
|
10
|
+
const result = {}
|
|
11
|
+
const raw = model._raw
|
|
12
|
+
|
|
13
|
+
for (const key in raw) {
|
|
14
|
+
if (key !== '_changed' && key !== '_status') {
|
|
15
|
+
result[key] = raw[key]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return result as RawSerialized<TModel>
|
|
20
|
+
}
|
|
21
|
+
}
|