@shaxpir/duiduidui-models 1.3.2 → 1.3.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.
@@ -1,325 +0,0 @@
1
- import { Doc } from '@shaxpir/sharedb/lib/client';
2
- import { CompactDateTime, Dispatch, DispatchTopic, Struct } from '@shaxpir/shaxpir-common';
3
- import { ShareSync } from '../repo';
4
- import { ContentId, ContentMeta, ContentRef } from "./Content";
5
- import { ContentKind } from './ContentKind';
6
- import { ManifestBody } from './Manifest';
7
- import { PermissionType } from './Permissions';
8
- import { ChangeModel } from './ChangeModel';
9
-
10
- export abstract class Model {
11
-
12
- public static CHANGED:DispatchTopic = "MODEL_CHANGED";
13
-
14
- public doc:Doc;
15
- private _isDisposed:boolean;
16
-
17
- // The "forbidden" flag is set when the user does not have permission to read or write the model.
18
- // We set this flag during fetch, subscribe, and ensureData operations, if the server responds with
19
- // 'forbidden'. Rather than throwing an exception or emitting an error event, or calling an error
20
- // callback, this technique allows the nornal flow of logic to proceed, and the model can be checked
21
- // for the forbidden flag at any time. Callers can (for example) display a 404 page, or a "forbidden"
22
- // message, or simply ignore the model, depending on the context, rather than having to handle errors.
23
- // But it should be forewarned that any BatchOperation that is attempted on a forbidden model will
24
- // throw an exception, because the model data is not allowed to be read or written by this user.
25
- private _isForbidden:boolean;
26
-
27
- private _shouldPerformModelChangeAnalysis:boolean;
28
- private _dataBeforeOpBatch:any = null;
29
- private _clearEventListeners:() => void = null;
30
-
31
- protected shareSync:ShareSync;
32
- private _subscribingPromise:Promise<Model> = null;
33
- private _acquireCount:number = 0;
34
-
35
- constructor(doc:Doc, shouldAcquire:boolean, shareSync:ShareSync) {
36
- const model = this;
37
- model.doc = doc;
38
- model.shareSync = shareSync;
39
- model._isDisposed = false;
40
- model._isForbidden = false;
41
- // When a model is newly created, or retrieved via a "fetch query", treat that as an implicit
42
- // call to "acquire", since the SharedDB doc object will eventually need to be disposed.
43
- if (shouldAcquire) {
44
- this._acquireCount++;
45
- }
46
- const onBeforeOpBatch = function (op:any, source:any) {
47
- if (model._shouldPerformModelChangeAnalysis) {
48
- model._dataBeforeOpBatch = Struct.clone(model.doc.data);
49
- }
50
- };
51
- const onOpBatch = function (op:any, source:any) {
52
- if (model._shouldPerformModelChangeAnalysis) {
53
- // Use tuple-arrays to perform a diff...
54
- const dataAfterOpBatch = Struct.clone(model.doc.data);
55
- let changeItems = ChangeModel.between(model._dataBeforeOpBatch, dataAfterOpBatch);
56
- model._dataBeforeOpBatch = null;
57
- // FROM https://share.github.io/sharedb/api/doc ...
58
- // The 'source' value will be false for remote ops received from other clients, or will be truthy
59
- // for ops submitted from this doc instance. For local ops, it will be the value of source supplied
60
- // to submitOp, or true if no value was supplied
61
- const isLocalChange = !!source;
62
- const change = {
63
- local: isLocalChange,
64
- model: model,
65
- items: changeItems,
66
- op: op
67
- };
68
- Dispatch.publish(Model.CHANGED, change);
69
- }
70
- };
71
- model.doc.on('before op batch', onBeforeOpBatch);
72
- model.doc.on('op batch', onOpBatch);
73
- this._clearEventListeners = function () {
74
- if (model.doc && model.doc.off) {
75
- model.doc.off('before op batch', onBeforeOpBatch);
76
- model.doc.off('op batch', onOpBatch);
77
- }
78
- };
79
- }
80
-
81
- public get kind():ContentKind {
82
- return this.doc.collection as ContentKind;
83
- }
84
-
85
- public get ref():ContentRef {
86
- return this.doc.id as ContentRef;
87
- }
88
-
89
- public get compoundKey():string {
90
- return `${this.doc.collection}/${this.doc.id}`;
91
- }
92
-
93
- public exists():boolean {
94
- return (!!this.doc.type) && !this.isForbidden;
95
- }
96
-
97
- public get isForbidden():boolean {
98
- return this._isForbidden;
99
- }
100
-
101
- public get isSubscribed():boolean {
102
- return this.doc.subscribed;
103
- }
104
-
105
- public get acquireCount():number {
106
- return this._acquireCount;
107
- }
108
-
109
- public async acquire(minVersion?:CompactDateTime):Promise<Model> {
110
- this._acquireCount++;
111
- if (minVersion) {
112
- this.log(`ACQUIRE: (${this.compoundKey}) at minVersion '${minVersion}'; acquireCount = ${this.acquireCount}`);
113
- return this.ensureRecentData(minVersion);
114
- } else {
115
- this.log(`ACQUIRE: (${this.compoundKey}); acquireCount = ${this.acquireCount}`);
116
- return this.ensureData();
117
- }
118
- }
119
-
120
- public release():void {
121
- this._acquireCount--;
122
- this.log(`RELEASE: (${this.compoundKey}); acquireCount = ${this.acquireCount}`);
123
- if (this._acquireCount <= 0) {
124
- this._acquireCount = 0;
125
- this.dispose()
126
- }
127
- }
128
-
129
- public dispose():void {
130
- const model = this;
131
- model._clearEventListeners();
132
- model.shareSync.dispose(model);
133
- }
134
-
135
- public disposeAndDestroy():void {
136
- this.log(`DISPOSE (AND DESTROY): (${this.compoundKey})`);
137
- this._isDisposed = true;
138
- this.doc.destroy();
139
- }
140
-
141
- public disposeAndUnsubscribe():void {
142
- this.log(`DISPOSE (AND UNSUBSCRIBE): (${this.compoundKey})`);
143
- this._isDisposed = true;
144
- if (this.isSubscribed) {
145
- this.unsubscribe();
146
- }
147
- }
148
-
149
- public async fetch():Promise<Model> {
150
- this.log(`FETCH: (${this.compoundKey})`);
151
- const model = this;
152
- return new Promise((resolve, reject) => {
153
- model.doc.fetch((fetchErr:any) => {
154
- if (fetchErr) {
155
- if (fetchErr.message == 'forbidden') {
156
- model._isForbidden = true;
157
- resolve(model);
158
- } else {
159
- const message = `error fetching model ${model.compoundKey}: ${fetchErr}`;
160
- console.log(message);
161
- model.release();
162
- reject(message);
163
- }
164
- } else {
165
- model.shareSync.isDebug() && console.log(`fetched model ${model.compoundKey}`);
166
- resolve(model);
167
- }
168
- });
169
- });
170
- }
171
-
172
- public async subscribe():Promise<Model> {
173
- this.log(`SUBSCRIBE: (${this.compoundKey})`);
174
- if (this._subscribingPromise != null) {
175
- return this._subscribingPromise;
176
- }
177
- if (this.isSubscribed) {
178
- return this;
179
- }
180
- const model = this;
181
- this._subscribingPromise = new Promise((resolve, reject) => {
182
- this.doc.subscribe((subscribeErr:any) => {
183
- model._subscribingPromise = null;
184
- if (subscribeErr) {
185
- if (subscribeErr.message == 'forbidden') {
186
- model._isForbidden = true;
187
- resolve(model);
188
- } else {
189
- const message = `error subscribing to Model ${model.compoundKey}: ${subscribeErr}`;
190
- console.log(message);
191
- reject(message);
192
- }
193
- } else {
194
- model.shareSync.isDebug() && console.log(`subscribed to Model ${model.compoundKey}`);
195
- resolve(model);
196
- }
197
- });
198
- });
199
- return this._subscribingPromise;
200
- }
201
-
202
- public async unsubscribe():Promise<Model> {
203
- this.log(`UNSUBSCRIBE: (${this.compoundKey})`);
204
- if (this._subscribingPromise != null) {
205
- await this._subscribingPromise;
206
- }
207
- if (!this.isSubscribed) {
208
- return this;
209
- }
210
- const model = this;
211
- return new Promise((resolve, reject) => {
212
- this.doc.unsubscribe((unsubscribeErr:any) => {
213
- if (unsubscribeErr) {
214
- const message = `error unsubscribing from Model ${model.compoundKey}: ${unsubscribeErr}`;
215
- console.log(message);
216
- reject(message);
217
- } else {
218
- model.shareSync.isDebug() && console.log(`subscribed to Model ${model.compoundKey}`);
219
- resolve(model);
220
- }
221
- });
222
- });
223
- }
224
-
225
- public isDisposed():boolean {
226
- return this._isDisposed;
227
- }
228
-
229
- public checkDisposed(source:string):void {
230
- if (this._isDisposed) {
231
- console.error(`*** MODEL ALREDY DISPOSED: (${this.compoundKey}); source = ${source}`);
232
- }
233
- }
234
-
235
- public async ensureData():Promise<Model> {
236
- const model = this;
237
- return new Promise((resolve, reject) => {
238
- this.doc.ensureDocHasData((ensureErr:any) => {
239
- if (ensureErr) {
240
- if (ensureErr.message == 'forbidden') {
241
- model._isForbidden = true;
242
- resolve(model);
243
- } else {
244
- const message = `error ensuring doc has data in Model ${model.compoundKey}: ${ensureErr}`;
245
- console.log(message);
246
- reject(message);
247
- }
248
- } else {
249
- model.shareSync.isDebug() && console.log(`ensured doc has data for Model ${model.compoundKey}`);
250
- resolve(model);
251
- }
252
- });
253
- });
254
- }
255
-
256
- public async ensureRecentData(minVersion:CompactDateTime):Promise<Model> {
257
- const model = this;
258
- return new Promise((resolve, reject) => {
259
- this.doc.ensureDocHasRecentData(minVersion, (ensureErr:any) => {
260
- if (ensureErr) {
261
- if (ensureErr.message == 'forbidden') {
262
- model._isForbidden = true;
263
- resolve(model);
264
- } else {
265
- const message = `error ensuring doc has recent data in Model ${model.compoundKey} with minVersion ${minVersion}: ${ensureErr}`;
266
- console.log(message);
267
- reject(message);
268
- }
269
- } else {
270
- model.shareSync.isDebug() && console.log(`ensured doc has recent data for Model ${model.compoundKey} with minVersion ${minVersion}`);
271
- resolve(model);
272
- }
273
- });
274
- });
275
- }
276
-
277
- public doesUserHavePermission(
278
- userId:ContentId,
279
- type:PermissionType
280
- ):boolean {
281
- this.checkDisposed("Model.doesUserHavePermission");
282
- if (this.kind === ContentKind.MANIFEST) {
283
- return Model.doesUserHaveManifestPermission(this.doc.data, userId, type);
284
- } else {
285
- // Content objects and checkpoints all have a PermissionsMeta
286
- const meta = this.doc.data.meta as ContentMeta;
287
- return Model.doesUserHaveContentPermission(this.kind, meta, userId, type);
288
- }
289
- }
290
-
291
- public static doesUserHaveManifestPermission(
292
- data:ManifestBody,
293
- userId:ContentId,
294
- type:PermissionType
295
- ):boolean {
296
- return data.meta.owner === userId;
297
- }
298
-
299
- public static doesUserHaveContentPermission(
300
- kind:ContentKind,
301
- meta:ContentMeta,
302
- userId:ContentId,
303
- type:PermissionType
304
- ):boolean {
305
-
306
- // Users are allowed to read any of their own content, but they are not allowed to write their User or Billing objects.
307
- if (meta.owner === userId) {
308
- if (type === PermissionType.READ) {
309
- return true;
310
- } else if (type === PermissionType.WRITE) {
311
- return kind !== ContentKind.USER && kind !== ContentKind.BILLING;
312
- }
313
- }
314
-
315
- // If all else fails, deny permission.
316
- return false;
317
- }
318
-
319
- private log(message:string):void {
320
- if (this.shareSync.isDebug()) {
321
- console.log(message);
322
- }
323
- }
324
-
325
- }
@@ -1,205 +0,0 @@
1
- import { Doc } from '@shaxpir/sharedb/lib/client';
2
- import { MultiClock, MultiTime, Struct } from '@shaxpir/shaxpir-common';
3
- import { TextEditOps } from '../repo/TextEditOps';
4
-
5
- import * as Json1 from '../repo/PermissiveJson1';
6
- import { Model } from './Model';
7
- import { ShareSyncFactory } from '../repo';
8
- import { Content } from './Content';
9
-
10
- const UnicodeText = require('ot-text-unicode');
11
-
12
- export type JsonPathElement = string | number;
13
- export type JsonPath = JsonPathElement[];
14
-
15
- export interface PathVal {
16
- path:JsonPath,
17
- val:any
18
- };
19
-
20
- export class JsonPathSelect {
21
-
22
- public static getValueAtPath(doc:Doc, path:JsonPath):any {
23
- let currentNode = doc.data;
24
- for (let i = 0; i < path.length; i++) {
25
- let pathElement:JsonPathElement = path[i];
26
- if (currentNode !== undefined && currentNode !== null) {
27
- if (typeof(pathElement) === "string" && currentNode.hasOwnProperty(pathElement)) {
28
- currentNode = currentNode[pathElement];
29
- } else if (typeof(pathElement) === "number" && currentNode.length > pathElement) {
30
- currentNode = currentNode[pathElement];
31
- } else {
32
- return undefined;
33
- }
34
- } else {
35
- console.error(`no value at path ${JSON.stringify(path)} at index ${i} in object '${doc.collection}/${doc.id}' with value ${JSON.stringify(doc.data)}`);
36
- break;
37
- }
38
- }
39
- return currentNode;
40
- }
41
- }
42
-
43
- export class BatchOperation {
44
-
45
- private model:Model;
46
- private time:MultiTime;
47
- private ops:any[];
48
-
49
- // TODO: maybe just use the timestamp on the Content?
50
- constructor(model:Model, time?:MultiTime) {
51
- if (model.isForbidden) {
52
- throw new Error(`cannot create a BatchOperation on forbidden model: ${model.compoundKey}`);
53
- }
54
- this.model = model;
55
- if (time) {
56
- this.time = time;
57
- } else {
58
- this.time = MultiClock.now()
59
- }
60
- this.ops = [];
61
- }
62
-
63
- public get at():MultiTime {
64
- return Struct.clone(this.time);
65
- }
66
-
67
- public hasOps():boolean {
68
- return this.ops.length > 0;
69
- }
70
-
71
- public editPathText(
72
- path:JsonPath,
73
- value:string
74
- ):void {
75
- const prevValue = JsonPathSelect.getValueAtPath(this.model.doc, path);
76
- if (prevValue !== undefined) {
77
- // If we're trying to edit a text value, but the previous value is null, then the
78
- // 'editOp' will throw an exception. So we need to use a 'replaceOp' instead.
79
- if (prevValue === null) {
80
- this.ops.push(Json1.replaceOp(path, prevValue, value));
81
- } else if (!Struct.equals(prevValue, value)) {
82
- const textEditOps = TextEditOps.between(prevValue, value);
83
- this.ops.push(Json1.editOp(path, UnicodeText.type, textEditOps));
84
- }
85
- } else {
86
- this.ops.push(Json1.insertOp(path, value));
87
- }
88
- }
89
-
90
- public setPathValue(
91
- path:JsonPath,
92
- value:any
93
- ):void {
94
- const prevValue = JsonPathSelect.getValueAtPath(this.model.doc, path);
95
- if (prevValue !== undefined) {
96
- if (!Struct.equals(prevValue, value)) {
97
- this.ops.push(Json1.replaceOp(path, prevValue, value));
98
- }
99
- } else {
100
- this.ops.push(Json1.insertOp(path, value));
101
- }
102
- }
103
-
104
- public move(
105
- originPath:JsonPath,
106
- destinationPath:JsonPath
107
- ):void {
108
- this.ops.push(Json1.moveOp(originPath, destinationPath));
109
- }
110
-
111
- public insertIntoArray(
112
- arrayPath:JsonPath,
113
- index:number,
114
- value:any
115
- ):void {
116
- let array = JsonPathSelect.getValueAtPath(this.model.doc, arrayPath);
117
- if (array === undefined) {
118
- this.ops.push(Json1.insertOp(arrayPath, [ value ]));
119
- } else {
120
- if (!Array.isArray(array)) {
121
- throw new Error(`path '${JSON.stringify(arrayPath)}' does not refer to an array: ${JSON.stringify(this.model.doc.data)}`);
122
- }
123
- const path = Struct.clone(arrayPath);
124
- path.push(index);
125
- this.ops.push(Json1.insertOp(path, value));
126
- }
127
- }
128
-
129
- public unshiftIntoArray(
130
- path:JsonPath,
131
- value:any
132
- ):void {
133
- path = Struct.clone(path);
134
- let array = JsonPathSelect.getValueAtPath(this.model.doc, path);
135
- if (array === undefined) {
136
- this.ops.push(Json1.insertOp(path, [ value ]));
137
- } else {
138
- if (!Array.isArray(array)) {
139
- throw new Error(`path '${JSON.stringify(path)}' does not refer to an array: ${JSON.stringify(this.model.doc.data)}`);
140
- }
141
- path.push(0);
142
- this.ops.push(Json1.insertOp(path, value));
143
- }
144
- }
145
-
146
- public pushIntoArray(
147
- path:JsonPath,
148
- value:any
149
- ):void {
150
- path = Struct.clone(path);
151
- let array = JsonPathSelect.getValueAtPath(this.model.doc, path);
152
- if (array === undefined) {
153
- this.ops.push(Json1.insertOp(path, [ value ]));
154
- } else {
155
- if (!Array.isArray(array)) {
156
- throw new Error(`path '${JSON.stringify(path)}' does not refer to an array: ${JSON.stringify(this.model.doc.data)}`);
157
- }
158
- path.push(array.length);
159
- this.ops.push(Json1.insertOp(path, value));
160
- }
161
- }
162
-
163
- public removeValueAtPath(
164
- path:JsonPath
165
- ):void {
166
- let prevValue:any = JsonPathSelect.getValueAtPath(this.model.doc, path);
167
- if (prevValue !== undefined) {
168
- this.ops.push(Json1.removeOp(path, prevValue));
169
- }
170
- }
171
-
172
- public replaceValueAtPath(
173
- path:JsonPath,
174
- value:any
175
- ):void {
176
- let prevValue:any = JsonPathSelect.getValueAtPath(this.model.doc, path);
177
- if (prevValue !== undefined) {
178
- this.ops.push(Json1.replaceOp(path, prevValue, value));
179
- } else {
180
- this.ops.push(Json1.insertOp(path, value));
181
- }
182
- }
183
-
184
- public commit():void {
185
- const batch = this;
186
- if (batch.ops.length > 0) {
187
- // Update the timestamp of the model (omitting local-time for Manifest objects (which are not Content)).
188
- this.setPathValue([ 'meta', 'updated_at', 'utc_time' ], batch.time.utc_time);
189
- if (batch.model instanceof Content) {
190
- this.setPathValue([ 'meta', 'updated_at', 'local_time' ], batch.time.local_time);
191
- }
192
- const reduced = batch.ops.reduce(Json1.type.compose, null);
193
- const shareSync = ShareSyncFactory.get();
194
- batch.model.doc.submitOp(reduced, (error:any) => {
195
- if (error) {
196
- shareSync.onOperationError(error);
197
- }
198
- });
199
- if (batch.model instanceof Content) {
200
- (batch.model as Content).modelUpdated();
201
- }
202
- }
203
- }
204
-
205
- }
@@ -1,19 +0,0 @@
1
- export enum PermissionType {
2
- READ,
3
- WRITE
4
- }
5
-
6
- export class PermissionModel {
7
-
8
- public static shouldAllow(
9
- grantedPermission:PermissionType,
10
- necessaryPermission:PermissionType
11
- ):boolean {
12
- if (grantedPermission == necessaryPermission) {
13
- return true;
14
- } else if (grantedPermission == PermissionType.WRITE && necessaryPermission == PermissionType.READ) {
15
- return true;
16
- }
17
- return false;
18
- }
19
- }
@@ -1,53 +0,0 @@
1
- import { CompactDateTime } from "@shaxpir/shaxpir-common";
2
- import { ContentId } from "./Content";
3
- import { ReviewLike } from "./Review";
4
-
5
- export interface PhraseExample {
6
- id: number;
7
- text: string;
8
- pinyin: string;
9
- translation: string;
10
- learn_rank: number;
11
- sense_rank: number;
12
- }
13
-
14
- export interface Phrase {
15
- text: string;
16
- hanzi_count: number;
17
- sense_rank: number;
18
- learn_rank: number;
19
- pinyin: string;
20
- pinyin_tokenized: string;
21
- transliteration: string;
22
- translation: string;
23
- notes: string;
24
- examples?: PhraseExample[];
25
- components?: PhraseExample[];
26
- keywords?: string[];
27
- tags?: string[];
28
- }
29
-
30
- export interface BuiltInPhrase extends Phrase {
31
- id: number;
32
- }
33
-
34
- export interface AnnotatedPhrase extends Phrase {
35
-
36
- // Always populated. If this is a builtin entry, it will be `phrase-${phrase_id}`.
37
- // If this is a user-specific entry, it will be `term-${term_id}`.
38
- id: string;
39
-
40
- // if this is populated, the Phrase comes from the built-in database
41
- phrase_id?: number;
42
-
43
- // if this is populated, then there is a user-specific Term model associated with this Phrase
44
- content_id?: ContentId;
45
-
46
- // All these fields come from the Term model, if it exists. If it doesn't exist, then these fields
47
- // have default values (null for starred_at, numbers are zero, arrays are empty, etc.)
48
- starred_at: CompactDateTime | null;
49
- alpha: number;
50
- beta: number;
51
- proficiency: number;
52
- reviews: ReviewLike[];
53
- }