@shaxpir/duiduidui-models 1.3.2 → 1.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/models/Model.d.ts +2 -0
- package/dist/models/Model.js +13 -2
- package/dist/repo/ShareSync.js +24 -5
- package/package.json +5 -1
- package/lib/index.ts +0 -7
- package/lib/models/ArrayView.ts +0 -203
- package/lib/models/BayesianScore.ts +0 -32
- package/lib/models/ChangeModel.ts +0 -94
- package/lib/models/Content.ts +0 -107
- package/lib/models/ContentKind.ts +0 -19
- package/lib/models/Device.ts +0 -90
- package/lib/models/GeoLocation.ts +0 -4
- package/lib/models/Hanzi.ts +0 -16
- package/lib/models/Manifest.ts +0 -171
- package/lib/models/Media.ts +0 -125
- package/lib/models/Metric.ts +0 -233
- package/lib/models/Model.ts +0 -325
- package/lib/models/Operation.ts +0 -205
- package/lib/models/Permissions.ts +0 -19
- package/lib/models/Phrase.ts +0 -53
- package/lib/models/Profile.ts +0 -117
- package/lib/models/Progress.ts +0 -101
- package/lib/models/Review.ts +0 -18
- package/lib/models/Session.ts +0 -149
- package/lib/models/Term.ts +0 -202
- package/lib/models/User.ts +0 -97
- package/lib/models/Workspace.ts +0 -129
- package/lib/models/index.ts +0 -24
- package/lib/repo/ConnectionListener.ts +0 -25
- package/lib/repo/PermissiveJson1.ts +0 -14
- package/lib/repo/ShareSync.ts +0 -383
- package/lib/repo/TextEditOps.ts +0 -50
- package/lib/repo/index.ts +0 -6
- package/lib/util/Encryption.ts +0 -5
- package/lib/util/Logging.ts +0 -568
- package/lib/util/index.ts +0 -4
- package/tsconfig.json +0 -25
- package/tslint.json +0 -46
package/lib/models/Model.ts
DELETED
|
@@ -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
|
-
}
|
package/lib/models/Operation.ts
DELETED
|
@@ -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
|
-
}
|
package/lib/models/Phrase.ts
DELETED
|
@@ -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
|
-
}
|