document-drive 1.0.0-alpha.92 → 1.0.0-alpha.93
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/package.json +4 -2
- package/src/read-mode/errors.ts +19 -0
- package/src/read-mode/index.ts +127 -0
- package/src/read-mode/service.ts +212 -0
- package/src/read-mode/types.ts +103 -0
- package/src/server/error.ts +8 -0
- package/src/server/index.ts +28 -11
- package/src/server/listener/transmitter/internal.ts +3 -3
- package/src/server/listener/transmitter/pull-responder.ts +8 -3
- package/src/server/listener/transmitter/switchboard-push.ts +3 -3
- package/src/server/types.ts +41 -11
- package/src/storage/base.ts +84 -0
- package/src/storage/index.ts +1 -0
- package/src/utils/default-drives-manager.ts +87 -14
- package/src/utils/graphql.ts +250 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "document-drive",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.93",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./src/index.ts",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"sqlite3": "^5.1.7"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
+
"change-case": "^5.4.4",
|
|
49
50
|
"exponential-backoff": "^3.1.1",
|
|
50
51
|
"graphql": "^16.9.0",
|
|
51
52
|
"graphql-request": "^6.1.0",
|
|
@@ -82,7 +83,8 @@
|
|
|
82
83
|
"sqlite3": "^5.1.7",
|
|
83
84
|
"typescript": "^5.5.3",
|
|
84
85
|
"vitest": "^2.0.5",
|
|
85
|
-
"webdriverio": "^9.0.9"
|
|
86
|
+
"webdriverio": "^9.0.9",
|
|
87
|
+
"vitest-fetch-mock": "^0.3.0"
|
|
86
88
|
},
|
|
87
89
|
"packageManager": "pnpm@9.1.4+sha256.30a1801ac4e723779efed13a21f4c39f9eb6c9fbb4ced101bce06b422593d7c9"
|
|
88
90
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export abstract class ReadDriveError extends Error {}
|
|
2
|
+
|
|
3
|
+
export class ReadDriveNotFoundError extends ReadDriveError {
|
|
4
|
+
constructor(driveId: string) {
|
|
5
|
+
super(`Read drive ${driveId} not found.`);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class ReadDriveSlugNotFoundError extends ReadDriveError {
|
|
10
|
+
constructor(slug: string) {
|
|
11
|
+
super(`Read drive with slug ${slug} not found.`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class ReadDocumentNotFoundError extends ReadDriveError {
|
|
16
|
+
constructor(drive: string, id: string) {
|
|
17
|
+
super(`Document with id ${id} not found on read drive ${drive}.`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { ListenerFilter } from 'document-model-libs/document-drive';
|
|
2
|
+
import { Document } from 'document-model/document';
|
|
3
|
+
import { DocumentDriveServerConstructor, RemoteDriveOptions } from '../server';
|
|
4
|
+
import { logger } from '../utils/logger';
|
|
5
|
+
import { ReadDriveSlugNotFoundError } from './errors';
|
|
6
|
+
import { ReadModeService } from './service';
|
|
7
|
+
import {
|
|
8
|
+
IReadModeDriveServer,
|
|
9
|
+
IReadModeDriveService,
|
|
10
|
+
ReadDrive,
|
|
11
|
+
ReadDrivesListener,
|
|
12
|
+
ReadModeDriveServerMixin
|
|
13
|
+
} from './types';
|
|
14
|
+
|
|
15
|
+
export * from './errors';
|
|
16
|
+
export * from './types';
|
|
17
|
+
|
|
18
|
+
export function ReadModeServer<TBase extends DocumentDriveServerConstructor>(
|
|
19
|
+
Base: TBase
|
|
20
|
+
): ReadModeDriveServerMixin {
|
|
21
|
+
return class ReadMode extends Base implements IReadModeDriveServer {
|
|
22
|
+
#readModeStorage: IReadModeDriveService;
|
|
23
|
+
#listeners = new Set<ReadDrivesListener>();
|
|
24
|
+
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
|
+
constructor(...args: any[]) {
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
28
|
+
super(...args);
|
|
29
|
+
|
|
30
|
+
this.#readModeStorage = new ReadModeService(
|
|
31
|
+
this.getDocumentModel.bind(this)
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
this.#buildDrives()
|
|
35
|
+
.then(drives => {
|
|
36
|
+
if (drives.length) {
|
|
37
|
+
this.#notifyListeners(drives, 'add');
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
.catch(logger.error);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async #buildDrives() {
|
|
44
|
+
const driveIds = await this.getReadDrives();
|
|
45
|
+
const drives = (
|
|
46
|
+
await Promise.all(
|
|
47
|
+
driveIds.map(driveId => this.getReadDrive(driveId))
|
|
48
|
+
)
|
|
49
|
+
).filter(drive => !(drive instanceof Error)) as ReadDrive[];
|
|
50
|
+
return drives;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#notifyListeners(drives: ReadDrive[], operation: 'add' | 'delete') {
|
|
54
|
+
this.#listeners.forEach(listener => listener(drives, operation));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getReadDrives(): Promise<string[]> {
|
|
58
|
+
return this.#readModeStorage.getReadDrives();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
getReadDrive(id: string) {
|
|
62
|
+
return this.#readModeStorage.getReadDrive(id);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
getReadDriveBySlug(
|
|
66
|
+
slug: string
|
|
67
|
+
): Promise<ReadDrive | ReadDriveSlugNotFoundError> {
|
|
68
|
+
return this.#readModeStorage.getReadDriveBySlug(slug);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
getReadDriveContext(id: string) {
|
|
72
|
+
return this.#readModeStorage.getReadDriveContext(id);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async addReadDrive(url: string, filter?: ListenerFilter) {
|
|
76
|
+
await this.#readModeStorage.addReadDrive(url, filter);
|
|
77
|
+
this.#notifyListeners(await this.#buildDrives(), 'add');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
fetchDrive(id: string) {
|
|
81
|
+
return this.#readModeStorage.fetchDrive(id);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
fetchDocument<D extends Document>(
|
|
85
|
+
driveId: string,
|
|
86
|
+
documentId: string,
|
|
87
|
+
documentType: string
|
|
88
|
+
) {
|
|
89
|
+
return this.#readModeStorage.fetchDocument<D>(
|
|
90
|
+
driveId,
|
|
91
|
+
documentId,
|
|
92
|
+
documentType
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async deleteReadDrive(id: string) {
|
|
97
|
+
const error = await this.#readModeStorage.deleteReadDrive(id);
|
|
98
|
+
if (error) {
|
|
99
|
+
return error;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.#notifyListeners(await this.#buildDrives(), 'delete');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async migrateReadDrive(id: string, options: RemoteDriveOptions) {
|
|
106
|
+
const result = await this.getReadDriveContext(id);
|
|
107
|
+
if (result instanceof Error) {
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const newDrive = await this.addRemoteDrive(result.url, options);
|
|
113
|
+
return newDrive;
|
|
114
|
+
} catch (error) {
|
|
115
|
+
// if an error is thrown, then add the read drive again
|
|
116
|
+
logger.error(error);
|
|
117
|
+
await this.addReadDrive(result.url, result.filter);
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
onReadDrivesUpdate(listener: ReadDrivesListener) {
|
|
123
|
+
this.#listeners.add(listener);
|
|
124
|
+
return Promise.resolve(() => this.#listeners.delete(listener));
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DocumentDriveDocument,
|
|
3
|
+
ListenerFilter
|
|
4
|
+
} from 'document-model-libs/document-drive';
|
|
5
|
+
import * as DocumentDrive from 'document-model-libs/document-drive';
|
|
6
|
+
import { Document, DocumentModel } from 'document-model/document';
|
|
7
|
+
import { GraphQLError } from 'graphql';
|
|
8
|
+
import { DocumentModelNotFoundError } from '../server/error';
|
|
9
|
+
import { fetchDocument, requestPublicDrive } from '../utils/graphql';
|
|
10
|
+
import {
|
|
11
|
+
ReadDocumentNotFoundError,
|
|
12
|
+
ReadDriveError,
|
|
13
|
+
ReadDriveNotFoundError,
|
|
14
|
+
ReadDriveSlugNotFoundError
|
|
15
|
+
} from './errors';
|
|
16
|
+
import {
|
|
17
|
+
GetDocumentModel,
|
|
18
|
+
InferDocumentLocalState,
|
|
19
|
+
InferDocumentOperation,
|
|
20
|
+
InferDocumentState,
|
|
21
|
+
IReadModeDriveService,
|
|
22
|
+
ReadDrive,
|
|
23
|
+
ReadDriveContext
|
|
24
|
+
} from './types';
|
|
25
|
+
|
|
26
|
+
export class ReadModeService implements IReadModeDriveService {
|
|
27
|
+
#getDocumentModel: GetDocumentModel;
|
|
28
|
+
#drives = new Map<
|
|
29
|
+
string,
|
|
30
|
+
{ drive: Omit<ReadDrive, 'readContext'>; context: ReadDriveContext }
|
|
31
|
+
>();
|
|
32
|
+
|
|
33
|
+
constructor(getDocumentModel: GetDocumentModel) {
|
|
34
|
+
this.#getDocumentModel = getDocumentModel;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#parseGraphQLErrors(
|
|
38
|
+
errors: GraphQLError[],
|
|
39
|
+
driveId: string,
|
|
40
|
+
documentId?: string
|
|
41
|
+
) {
|
|
42
|
+
for (const error of errors) {
|
|
43
|
+
if (error.message === `Drive with id ${driveId} not found`) {
|
|
44
|
+
return new ReadDriveNotFoundError(driveId);
|
|
45
|
+
} else if (
|
|
46
|
+
documentId &&
|
|
47
|
+
error.message === `Document with id ${documentId} not found`
|
|
48
|
+
) {
|
|
49
|
+
return new ReadDocumentNotFoundError(driveId, documentId);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const firstError = errors.at(0);
|
|
53
|
+
if (firstError) {
|
|
54
|
+
return firstError;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async #fetchDrive(id: string, url: string) {
|
|
59
|
+
const { errors, document } = await fetchDocument<DocumentDriveDocument>(
|
|
60
|
+
url,
|
|
61
|
+
id,
|
|
62
|
+
DocumentDrive
|
|
63
|
+
);
|
|
64
|
+
const error = errors ? this.#parseGraphQLErrors(errors, id) : undefined;
|
|
65
|
+
return error || document;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async fetchDrive(id: string): Promise<ReadDrive | ReadDriveNotFoundError> {
|
|
69
|
+
const drive = this.#drives.get(id);
|
|
70
|
+
if (!drive) {
|
|
71
|
+
return new ReadDriveNotFoundError(id);
|
|
72
|
+
}
|
|
73
|
+
const document = await this.fetchDocument<DocumentDriveDocument>(
|
|
74
|
+
id,
|
|
75
|
+
id,
|
|
76
|
+
DocumentDrive.documentModel.id
|
|
77
|
+
);
|
|
78
|
+
if (document instanceof Error) {
|
|
79
|
+
return document;
|
|
80
|
+
}
|
|
81
|
+
const result = { ...document, readContext: drive.context };
|
|
82
|
+
drive.drive = result;
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async fetchDocument<D extends Document>(
|
|
87
|
+
driveId: string,
|
|
88
|
+
documentId: string,
|
|
89
|
+
documentType: DocumentModel<
|
|
90
|
+
InferDocumentState<D>,
|
|
91
|
+
InferDocumentOperation<D>,
|
|
92
|
+
InferDocumentLocalState<D>
|
|
93
|
+
>['documentModel']['id']
|
|
94
|
+
): Promise<
|
|
95
|
+
| Document<
|
|
96
|
+
InferDocumentState<D>,
|
|
97
|
+
InferDocumentOperation<D>,
|
|
98
|
+
InferDocumentLocalState<D>
|
|
99
|
+
>
|
|
100
|
+
| DocumentModelNotFoundError
|
|
101
|
+
| ReadDriveNotFoundError
|
|
102
|
+
| ReadDocumentNotFoundError
|
|
103
|
+
> {
|
|
104
|
+
const drive = this.#drives.get(driveId);
|
|
105
|
+
if (!drive) {
|
|
106
|
+
return new ReadDriveNotFoundError(driveId);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let documentModel:
|
|
110
|
+
| DocumentModel<
|
|
111
|
+
InferDocumentState<D>,
|
|
112
|
+
InferDocumentOperation<D>,
|
|
113
|
+
InferDocumentLocalState<D>
|
|
114
|
+
>
|
|
115
|
+
| undefined = undefined;
|
|
116
|
+
try {
|
|
117
|
+
documentModel = this.#getDocumentModel(
|
|
118
|
+
documentType
|
|
119
|
+
) as unknown as DocumentModel<
|
|
120
|
+
InferDocumentState<D>,
|
|
121
|
+
InferDocumentOperation<D>,
|
|
122
|
+
InferDocumentLocalState<D>
|
|
123
|
+
>;
|
|
124
|
+
} catch (error) {
|
|
125
|
+
return new DocumentModelNotFoundError(documentType, error);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const { url } = drive.context;
|
|
129
|
+
const { errors, document } = await fetchDocument<D>(
|
|
130
|
+
url,
|
|
131
|
+
documentId,
|
|
132
|
+
documentModel
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
if (errors) {
|
|
136
|
+
const error = this.#parseGraphQLErrors(errors, driveId, documentId);
|
|
137
|
+
if (error instanceof ReadDriveError) {
|
|
138
|
+
return error;
|
|
139
|
+
} else if (error) {
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!document) {
|
|
145
|
+
return new ReadDocumentNotFoundError(driveId, documentId);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return document;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async addReadDrive(url: string, filter?: ListenerFilter): Promise<void> {
|
|
152
|
+
const { id } = await requestPublicDrive(url);
|
|
153
|
+
|
|
154
|
+
const result = await this.#fetchDrive(id, url);
|
|
155
|
+
if (result instanceof Error) {
|
|
156
|
+
throw result;
|
|
157
|
+
} else if (!result) {
|
|
158
|
+
throw new Error(`Drive "${id}" not found at ${url}`);
|
|
159
|
+
}
|
|
160
|
+
this.#drives.set(id, {
|
|
161
|
+
drive: result as unknown as ReadDrive,
|
|
162
|
+
context: {
|
|
163
|
+
url,
|
|
164
|
+
filter: filter ?? {
|
|
165
|
+
documentId: ['*'],
|
|
166
|
+
documentType: ['*'],
|
|
167
|
+
branch: ['*'],
|
|
168
|
+
scope: ['*']
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async getReadDrives(): Promise<string[]> {
|
|
175
|
+
return Promise.resolve([...this.#drives.keys()]);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async getReadDrive(id: string) {
|
|
179
|
+
const result = this.#drives.get(id);
|
|
180
|
+
return Promise.resolve(
|
|
181
|
+
result
|
|
182
|
+
? { ...result.drive, readContext: result.context }
|
|
183
|
+
: new ReadDriveNotFoundError(id)
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async getReadDriveBySlug(
|
|
188
|
+
slug: string
|
|
189
|
+
): Promise<ReadDrive | ReadDriveSlugNotFoundError> {
|
|
190
|
+
const readDrive = [...this.#drives.values()].find(
|
|
191
|
+
({ drive }) => drive.state.global.slug === slug
|
|
192
|
+
);
|
|
193
|
+
return Promise.resolve(
|
|
194
|
+
readDrive
|
|
195
|
+
? { ...readDrive.drive, readContext: readDrive.context }
|
|
196
|
+
: new ReadDriveSlugNotFoundError(slug)
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
getReadDriveContext(id: string) {
|
|
201
|
+
return Promise.resolve(
|
|
202
|
+
this.#drives.get(id)?.context ?? new ReadDriveNotFoundError(id)
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
deleteReadDrive(id: string): Promise<ReadDriveNotFoundError | undefined> {
|
|
207
|
+
const deleted = this.#drives.delete(id);
|
|
208
|
+
return Promise.resolve(
|
|
209
|
+
deleted ? undefined : new ReadDriveNotFoundError(id)
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DocumentDriveDocument,
|
|
3
|
+
ListenerFilter
|
|
4
|
+
} from 'document-model-libs/document-drive';
|
|
5
|
+
import { Action, Document, DocumentModel } from 'document-model/document';
|
|
6
|
+
import { DocumentDriveServerMixin, RemoteDriveOptions } from '../server';
|
|
7
|
+
import { DocumentModelNotFoundError } from '../server/error';
|
|
8
|
+
import {
|
|
9
|
+
ReadDocumentNotFoundError,
|
|
10
|
+
ReadDriveNotFoundError,
|
|
11
|
+
ReadDriveSlugNotFoundError
|
|
12
|
+
} from './errors';
|
|
13
|
+
|
|
14
|
+
// TODO: move these types to the document-model package
|
|
15
|
+
export type InferDocumentState<D extends Document> =
|
|
16
|
+
D extends Document<infer S> ? S : never;
|
|
17
|
+
|
|
18
|
+
export type InferDocumentOperation<D extends Document> =
|
|
19
|
+
D extends Document<unknown, infer A> ? A : never;
|
|
20
|
+
|
|
21
|
+
export type InferDocumentLocalState<D extends Document> =
|
|
22
|
+
D extends Document<unknown, Action, infer L> ? L : never;
|
|
23
|
+
|
|
24
|
+
export type InferDocumentGenerics<D extends Document> = {
|
|
25
|
+
state: InferDocumentState<D>;
|
|
26
|
+
action: InferDocumentOperation<D>;
|
|
27
|
+
logger: InferDocumentLocalState<D>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type ReadModeDriveServerMixin =
|
|
31
|
+
DocumentDriveServerMixin<IReadModeDriveServer>;
|
|
32
|
+
|
|
33
|
+
export type ReadDrivesListener = (
|
|
34
|
+
drives: ReadDrive[],
|
|
35
|
+
operation: 'add' | 'delete'
|
|
36
|
+
) => void;
|
|
37
|
+
|
|
38
|
+
export type ReadDrivesListenerUnsubscribe = () => void;
|
|
39
|
+
|
|
40
|
+
export interface IReadModeDriveServer extends IReadModeDriveService {
|
|
41
|
+
migrateReadDrive(
|
|
42
|
+
id: string,
|
|
43
|
+
options: RemoteDriveOptions
|
|
44
|
+
): Promise<DocumentDriveDocument | ReadDriveNotFoundError>;
|
|
45
|
+
onReadDrivesUpdate(
|
|
46
|
+
listener: ReadDrivesListener
|
|
47
|
+
): Promise<ReadDrivesListenerUnsubscribe>; // TODO: make DriveEvents extensible and reuse event emitter
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type ReadDriveContext = {
|
|
51
|
+
url: string;
|
|
52
|
+
filter: ListenerFilter;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type ReadDrive = DocumentDriveDocument & {
|
|
56
|
+
readContext: ReadDriveContext;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type IsDocument<D extends Document> =
|
|
60
|
+
(<G>() => G extends D ? 1 : 2) extends <G>() => G extends Document ? 1 : 2
|
|
61
|
+
? true
|
|
62
|
+
: false;
|
|
63
|
+
|
|
64
|
+
export interface IReadModeDriveService {
|
|
65
|
+
addReadDrive(url: string, filter?: ListenerFilter): Promise<void>;
|
|
66
|
+
|
|
67
|
+
getReadDrives(): Promise<string[]>;
|
|
68
|
+
|
|
69
|
+
getReadDriveBySlug(
|
|
70
|
+
slug: string
|
|
71
|
+
): Promise<ReadDrive | ReadDriveSlugNotFoundError>;
|
|
72
|
+
|
|
73
|
+
getReadDrive(id: string): Promise<ReadDrive | ReadDriveNotFoundError>;
|
|
74
|
+
|
|
75
|
+
getReadDriveContext(
|
|
76
|
+
id: string
|
|
77
|
+
): Promise<ReadDriveContext | ReadDriveNotFoundError>;
|
|
78
|
+
|
|
79
|
+
fetchDrive(id: string): Promise<ReadDrive | ReadDriveNotFoundError>;
|
|
80
|
+
|
|
81
|
+
fetchDocument<D extends Document>(
|
|
82
|
+
driveId: string,
|
|
83
|
+
documentId: string,
|
|
84
|
+
documentType: DocumentModel<
|
|
85
|
+
InferDocumentState<D>,
|
|
86
|
+
InferDocumentOperation<D>,
|
|
87
|
+
InferDocumentLocalState<D>
|
|
88
|
+
>['documentModel']['id']
|
|
89
|
+
): Promise<
|
|
90
|
+
| Document<
|
|
91
|
+
InferDocumentState<D>,
|
|
92
|
+
InferDocumentOperation<D>,
|
|
93
|
+
InferDocumentLocalState<D>
|
|
94
|
+
>
|
|
95
|
+
| DocumentModelNotFoundError
|
|
96
|
+
| ReadDriveNotFoundError
|
|
97
|
+
| ReadDocumentNotFoundError
|
|
98
|
+
>;
|
|
99
|
+
|
|
100
|
+
deleteReadDrive(id: string): Promise<ReadDriveNotFoundError | undefined>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export type GetDocumentModel = (documentType: string) => DocumentModel;
|
package/src/server/error.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { Operation } from 'document-model/document';
|
|
2
2
|
import type { ErrorStatus } from './types';
|
|
3
3
|
|
|
4
|
+
export class DocumentModelNotFoundError extends Error {
|
|
5
|
+
constructor(
|
|
6
|
+
public id: string,
|
|
7
|
+
cause?: unknown
|
|
8
|
+
) {
|
|
9
|
+
super(`Document model "${id}" not found`, { cause });
|
|
10
|
+
}
|
|
11
|
+
}
|
|
4
12
|
export class OperationError extends Error {
|
|
5
13
|
status: ErrorStatus;
|
|
6
14
|
operation: Operation | undefined;
|
package/src/server/index.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
Job,
|
|
36
36
|
OperationJob
|
|
37
37
|
} from '../queue/types';
|
|
38
|
+
import { ReadModeServer } from '../read-mode';
|
|
38
39
|
import { MemoryStorage } from '../storage/memory';
|
|
39
40
|
import type {
|
|
40
41
|
DocumentDriveStorage,
|
|
@@ -77,15 +78,17 @@ import {
|
|
|
77
78
|
StrandUpdateSource
|
|
78
79
|
} from './listener/transmitter';
|
|
79
80
|
import {
|
|
81
|
+
AbstractDocumentDriveServer,
|
|
80
82
|
AddOperationOptions,
|
|
81
|
-
BaseDocumentDriveServer,
|
|
82
83
|
DefaultListenerManagerOptions,
|
|
83
84
|
DocumentDriveServerOptions,
|
|
84
85
|
DriveEvents,
|
|
85
86
|
GetDocumentOptions,
|
|
86
87
|
GetStrandsOptions,
|
|
88
|
+
IBaseDocumentDriveServer,
|
|
87
89
|
IOperationResult,
|
|
88
90
|
ListenerState,
|
|
91
|
+
RemoteDriveAccessLevel,
|
|
89
92
|
RemoteDriveOptions,
|
|
90
93
|
StrandUpdate,
|
|
91
94
|
SynchronizationUnitQuery,
|
|
@@ -102,8 +105,14 @@ import { filterOperationsByRevision } from './utils';
|
|
|
102
105
|
export * from './listener';
|
|
103
106
|
export type * from './types';
|
|
104
107
|
|
|
108
|
+
export * from '../read-mode';
|
|
109
|
+
|
|
105
110
|
export const PULL_DRIVE_INTERVAL = 5000;
|
|
106
|
-
|
|
111
|
+
|
|
112
|
+
export class BaseDocumentDriveServer
|
|
113
|
+
extends AbstractDocumentDriveServer
|
|
114
|
+
implements IBaseDocumentDriveServer
|
|
115
|
+
{
|
|
107
116
|
private emitter = createNanoEvents<DriveEvents>();
|
|
108
117
|
private cache: ICache;
|
|
109
118
|
private documentModels: DocumentModel[];
|
|
@@ -131,11 +140,10 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
131
140
|
) {
|
|
132
141
|
super();
|
|
133
142
|
this.options = {
|
|
134
|
-
defaultRemoteDrives: [],
|
|
135
|
-
removeOldRemoteDrives: {
|
|
136
|
-
strategy: 'preserve-all'
|
|
137
|
-
},
|
|
138
143
|
...options,
|
|
144
|
+
defaultDrives: {
|
|
145
|
+
...options?.defaultDrives
|
|
146
|
+
},
|
|
139
147
|
listenerManager: {
|
|
140
148
|
...DefaultListenerManagerOptions,
|
|
141
149
|
...options?.listenerManager
|
|
@@ -180,6 +188,13 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
180
188
|
return this.defaultDrivesManager.getDefaultRemoteDrives();
|
|
181
189
|
}
|
|
182
190
|
|
|
191
|
+
setDefaultDriveAccessLevel(url: string, level: RemoteDriveAccessLevel) {
|
|
192
|
+
return this.defaultDrivesManager.setDefaultDriveAccessLevel(url, level);
|
|
193
|
+
}
|
|
194
|
+
setAllDefaultDrivesAccessLevel(level: RemoteDriveAccessLevel) {
|
|
195
|
+
return this.defaultDrivesManager.setAllDefaultDrivesAccessLevel(level);
|
|
196
|
+
}
|
|
197
|
+
|
|
183
198
|
private getOperationSource(source: StrandUpdateSource) {
|
|
184
199
|
return source.type === 'local' ? 'push' : 'pull';
|
|
185
200
|
}
|
|
@@ -899,7 +914,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
899
914
|
}));
|
|
900
915
|
}
|
|
901
916
|
|
|
902
|
-
|
|
917
|
+
protected getDocumentModel(documentType: string) {
|
|
903
918
|
const documentModel = this.documentModels.find(
|
|
904
919
|
model => model.documentModel.id === documentType
|
|
905
920
|
);
|
|
@@ -1093,7 +1108,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1093
1108
|
// if no document was provided then create a new one
|
|
1094
1109
|
const document =
|
|
1095
1110
|
input.document ??
|
|
1096
|
-
this.
|
|
1111
|
+
this.getDocumentModel(input.documentType).utils.createDocument();
|
|
1097
1112
|
|
|
1098
1113
|
// stores document information
|
|
1099
1114
|
const documentStorage: DocumentStorage = {
|
|
@@ -1331,7 +1346,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1331
1346
|
return documentStorage as T;
|
|
1332
1347
|
}
|
|
1333
1348
|
|
|
1334
|
-
const documentModel = this.
|
|
1349
|
+
const documentModel = this.getDocumentModel(
|
|
1335
1350
|
documentStorage.documentType
|
|
1336
1351
|
);
|
|
1337
1352
|
|
|
@@ -1369,7 +1384,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1369
1384
|
operation: Operation,
|
|
1370
1385
|
skipHashValidation = false
|
|
1371
1386
|
) {
|
|
1372
|
-
const documentModel = this.
|
|
1387
|
+
const documentModel = this.getDocumentModel(document.documentType);
|
|
1373
1388
|
|
|
1374
1389
|
const signalResults: SignalResult[] = [];
|
|
1375
1390
|
let newDocument = document;
|
|
@@ -2205,7 +2220,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
2205
2220
|
actions: (T | BaseAction)[]
|
|
2206
2221
|
): Operation<T | BaseAction>[] {
|
|
2207
2222
|
const operations: Operation<T | BaseAction>[] = [];
|
|
2208
|
-
const { reducer } = this.
|
|
2223
|
+
const { reducer } = this.getDocumentModel(document.documentType);
|
|
2209
2224
|
for (const action of actions) {
|
|
2210
2225
|
document = reducer(document, action);
|
|
2211
2226
|
const operation = document.operations[action.scope].slice().pop();
|
|
@@ -2369,3 +2384,5 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
2369
2384
|
return this.emitter.emit(event, ...args);
|
|
2370
2385
|
}
|
|
2371
2386
|
}
|
|
2387
|
+
|
|
2388
|
+
export const DocumentDriveServer = ReadModeServer(BaseDocumentDriveServer);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Document, OperationScope } from 'document-model/document';
|
|
2
2
|
import { logger } from '../../../utils/logger';
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
IBaseDocumentDriveServer,
|
|
5
5
|
Listener,
|
|
6
6
|
ListenerRevision,
|
|
7
7
|
OperationUpdate,
|
|
@@ -28,11 +28,11 @@ export type InternalTransmitterUpdate<
|
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
export class InternalTransmitter implements ITransmitter {
|
|
31
|
-
private drive:
|
|
31
|
+
private drive: IBaseDocumentDriveServer;
|
|
32
32
|
private listener: Listener;
|
|
33
33
|
private receiver: IReceiver | undefined;
|
|
34
34
|
|
|
35
|
-
constructor(listener: Listener, drive:
|
|
35
|
+
constructor(listener: Listener, drive: IBaseDocumentDriveServer) {
|
|
36
36
|
this.listener = listener;
|
|
37
37
|
this.drive = drive;
|
|
38
38
|
}
|
|
@@ -6,8 +6,8 @@ import { gql, requestGraphql } from '../../../utils/graphql';
|
|
|
6
6
|
import { logger as defaultLogger } from '../../../utils/logger';
|
|
7
7
|
import { OperationError } from '../../error';
|
|
8
8
|
import {
|
|
9
|
-
BaseDocumentDriveServer,
|
|
10
9
|
GetStrandsOptions,
|
|
10
|
+
IBaseDocumentDriveServer,
|
|
11
11
|
IOperationResult,
|
|
12
12
|
Listener,
|
|
13
13
|
ListenerRevision,
|
|
@@ -46,13 +46,13 @@ export interface IPullResponderTransmitter extends ITransmitter {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
export class PullResponderTransmitter implements IPullResponderTransmitter {
|
|
49
|
-
private drive:
|
|
49
|
+
private drive: IBaseDocumentDriveServer;
|
|
50
50
|
private listener: Listener;
|
|
51
51
|
private manager: ListenerManager;
|
|
52
52
|
|
|
53
53
|
constructor(
|
|
54
54
|
listener: Listener,
|
|
55
|
-
drive:
|
|
55
|
+
drive: IBaseDocumentDriveServer,
|
|
56
56
|
manager: ListenerManager
|
|
57
57
|
) {
|
|
58
58
|
this.listener = listener;
|
|
@@ -68,6 +68,11 @@ export class PullResponderTransmitter implements IPullResponderTransmitter {
|
|
|
68
68
|
);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
disconnect(): Promise<void> {
|
|
72
|
+
// TODO remove listener from switchboard
|
|
73
|
+
return Promise.resolve();
|
|
74
|
+
}
|
|
75
|
+
|
|
71
76
|
async processAcknowledge(
|
|
72
77
|
driveId: string,
|
|
73
78
|
listenerId: string,
|
|
@@ -2,7 +2,7 @@ import stringify from 'json-stringify-deterministic';
|
|
|
2
2
|
import { gql, requestGraphql } from '../../../utils/graphql';
|
|
3
3
|
import { logger } from '../../../utils/logger';
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
IBaseDocumentDriveServer,
|
|
6
6
|
Listener,
|
|
7
7
|
ListenerRevision,
|
|
8
8
|
StrandUpdate
|
|
@@ -10,11 +10,11 @@ import {
|
|
|
10
10
|
import { ITransmitter, StrandUpdateSource } from './types';
|
|
11
11
|
|
|
12
12
|
export class SwitchboardPushTransmitter implements ITransmitter {
|
|
13
|
-
private drive:
|
|
13
|
+
private drive: IBaseDocumentDriveServer;
|
|
14
14
|
private listener: Listener;
|
|
15
15
|
private targetURL: string;
|
|
16
16
|
|
|
17
|
-
constructor(listener: Listener, drive:
|
|
17
|
+
constructor(listener: Listener, drive: IBaseDocumentDriveServer) {
|
|
18
18
|
this.listener = listener;
|
|
19
19
|
this.drive = drive;
|
|
20
20
|
this.targetURL = listener.callInfo!.data!;
|
package/src/server/types.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
BaseAction,
|
|
14
14
|
CreateChildDocumentInput,
|
|
15
15
|
Document,
|
|
16
|
+
DocumentModel,
|
|
16
17
|
Operation,
|
|
17
18
|
OperationScope,
|
|
18
19
|
ReducerOptions,
|
|
@@ -20,7 +21,10 @@ import type {
|
|
|
20
21
|
State
|
|
21
22
|
} from 'document-model/document';
|
|
22
23
|
import { Unsubscribe } from 'nanoevents';
|
|
24
|
+
import { BaseDocumentDriveServer } from '.';
|
|
25
|
+
import { IReadModeDriveServer } from '../read-mode/types';
|
|
23
26
|
import { RunAsap } from '../utils';
|
|
27
|
+
import { IDefaultDrivesManager } from '../utils/default-drives-manager';
|
|
24
28
|
import { DriveInfo } from '../utils/graphql';
|
|
25
29
|
import { OperationError, SynchronizationUnitNotFoundError } from './error';
|
|
26
30
|
import {
|
|
@@ -29,16 +33,34 @@ import {
|
|
|
29
33
|
StrandUpdateSource
|
|
30
34
|
} from './listener/transmitter/types';
|
|
31
35
|
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any
|
|
37
|
+
export type Constructor<T = object> = new (...args: any[]) => T;
|
|
38
|
+
|
|
39
|
+
export type DocumentDriveServerConstructor =
|
|
40
|
+
Constructor<BaseDocumentDriveServer>;
|
|
41
|
+
|
|
42
|
+
// Mixin type that returns a type extending both the base class and the interface
|
|
43
|
+
export type Mixin<T extends Constructor, I> = T &
|
|
44
|
+
Constructor<InstanceType<T> & I>;
|
|
45
|
+
|
|
46
|
+
export type DocumentDriveServerMixin<I> = Mixin<
|
|
47
|
+
typeof BaseDocumentDriveServer,
|
|
48
|
+
I
|
|
49
|
+
>;
|
|
50
|
+
|
|
32
51
|
export type DriveInput = State<
|
|
33
52
|
Omit<DocumentDriveState, '__typename' | 'id' | 'nodes'> & { id?: string },
|
|
34
53
|
DocumentDriveLocalState
|
|
35
54
|
>;
|
|
36
55
|
|
|
56
|
+
export type RemoteDriveAccessLevel = 'READ' | 'WRITE';
|
|
57
|
+
|
|
37
58
|
export type RemoteDriveOptions = DocumentDriveLocalState & {
|
|
38
59
|
// TODO make local state optional
|
|
39
60
|
pullFilter?: ListenerFilter;
|
|
40
61
|
pullInterval?: number;
|
|
41
62
|
expectedDriveInfo?: DriveInfo;
|
|
63
|
+
accessLevel?: RemoteDriveAccessLevel;
|
|
42
64
|
};
|
|
43
65
|
|
|
44
66
|
export type CreateDocumentInput = CreateChildDocumentInput;
|
|
@@ -234,8 +256,10 @@ export type RemoveOldRemoteDrivesOption =
|
|
|
234
256
|
};
|
|
235
257
|
|
|
236
258
|
export type DocumentDriveServerOptions = {
|
|
237
|
-
|
|
238
|
-
|
|
259
|
+
defaultDrives: {
|
|
260
|
+
remoteDrives?: Array<DefaultRemoteDriveInput>;
|
|
261
|
+
removeOldRemoteDrives?: RemoveOldRemoteDrivesOption;
|
|
262
|
+
};
|
|
239
263
|
/* method to queue heavy tasks that might block the event loop.
|
|
240
264
|
* If set to null then it will queued as micro task.
|
|
241
265
|
* Defaults to the most appropriate method according to the system
|
|
@@ -250,7 +274,7 @@ export type GetStrandsOptions = {
|
|
|
250
274
|
fromRevision?: number;
|
|
251
275
|
};
|
|
252
276
|
|
|
253
|
-
export abstract class
|
|
277
|
+
export abstract class AbstractDocumentDriveServer {
|
|
254
278
|
/** Public methods **/
|
|
255
279
|
abstract getDrives(): Promise<string[]>;
|
|
256
280
|
abstract addDrive(drive: DriveInput): Promise<DocumentDriveDocument>;
|
|
@@ -416,6 +440,8 @@ export abstract class BaseDocumentDriveServer {
|
|
|
416
440
|
): Promise<Document>;
|
|
417
441
|
protected abstract deleteDocument(drive: string, id: string): Promise<void>;
|
|
418
442
|
|
|
443
|
+
protected abstract getDocumentModel(documentType: string): DocumentModel;
|
|
444
|
+
|
|
419
445
|
/** Event methods **/
|
|
420
446
|
protected abstract emit<K extends keyof DriveEvents>(
|
|
421
447
|
this: this,
|
|
@@ -450,17 +476,26 @@ export const DefaultListenerManagerOptions = {
|
|
|
450
476
|
sequentialUpdates: true
|
|
451
477
|
};
|
|
452
478
|
|
|
479
|
+
export type IBaseDocumentDriveServer = Pick<
|
|
480
|
+
AbstractDocumentDriveServer,
|
|
481
|
+
keyof AbstractDocumentDriveServer
|
|
482
|
+
>;
|
|
483
|
+
|
|
484
|
+
export type IDocumentDriveServer = IBaseDocumentDriveServer &
|
|
485
|
+
IDefaultDrivesManager &
|
|
486
|
+
IReadModeDriveServer;
|
|
487
|
+
|
|
453
488
|
export abstract class BaseListenerManager {
|
|
454
|
-
protected drive:
|
|
455
|
-
protected options: ListenerManagerOptions;
|
|
489
|
+
protected drive: IBaseDocumentDriveServer;
|
|
456
490
|
protected listenerState = new Map<string, Map<string, ListenerState>>();
|
|
491
|
+
protected options: ListenerManagerOptions;
|
|
457
492
|
protected transmitters: Record<
|
|
458
493
|
DocumentDriveState['id'],
|
|
459
494
|
Record<Listener['listenerId'], ITransmitter>
|
|
460
495
|
> = {};
|
|
461
496
|
|
|
462
497
|
constructor(
|
|
463
|
-
drive:
|
|
498
|
+
drive: IBaseDocumentDriveServer,
|
|
464
499
|
listenerState = new Map<string, Map<string, ListenerState>>(),
|
|
465
500
|
options: ListenerManagerOptions = DefaultListenerManagerOptions
|
|
466
501
|
) {
|
|
@@ -514,11 +549,6 @@ export abstract class BaseListenerManager {
|
|
|
514
549
|
): Promise<void>;
|
|
515
550
|
}
|
|
516
551
|
|
|
517
|
-
export type IDocumentDriveServer = Pick<
|
|
518
|
-
BaseDocumentDriveServer,
|
|
519
|
-
keyof BaseDocumentDriveServer
|
|
520
|
-
>;
|
|
521
|
-
|
|
522
552
|
export type ListenerStatus =
|
|
523
553
|
| 'CREATED'
|
|
524
554
|
| 'PENDING'
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { DocumentDriveAction } from 'document-model-libs/document-drive';
|
|
2
|
+
import { BaseAction, DocumentHeader, Operation } from 'document-model/document';
|
|
3
|
+
import { SynchronizationUnitQuery } from '../server';
|
|
4
|
+
import {
|
|
5
|
+
DocumentDriveStorage,
|
|
6
|
+
DocumentStorage,
|
|
7
|
+
IDriveStorage,
|
|
8
|
+
IStorage,
|
|
9
|
+
IStorageDelegate
|
|
10
|
+
} from './types';
|
|
11
|
+
|
|
12
|
+
abstract class BaseStorage implements IStorage {
|
|
13
|
+
abstract checkDocumentExists(drive: string, id: string): Promise<boolean>;
|
|
14
|
+
|
|
15
|
+
abstract getDocuments(drive: string): Promise<string[]>;
|
|
16
|
+
|
|
17
|
+
abstract getDocument(drive: string, id: string): Promise<DocumentStorage>;
|
|
18
|
+
|
|
19
|
+
abstract createDocument(
|
|
20
|
+
drive: string,
|
|
21
|
+
id: string,
|
|
22
|
+
document: DocumentStorage
|
|
23
|
+
): Promise<void>;
|
|
24
|
+
|
|
25
|
+
abstract addDocumentOperations(
|
|
26
|
+
drive: string,
|
|
27
|
+
id: string,
|
|
28
|
+
operations: Operation[],
|
|
29
|
+
header: DocumentHeader
|
|
30
|
+
): Promise<void>;
|
|
31
|
+
|
|
32
|
+
abstract addDocumentOperationsWithTransaction?(
|
|
33
|
+
drive: string,
|
|
34
|
+
id: string,
|
|
35
|
+
callback: (document: DocumentStorage) => Promise<{
|
|
36
|
+
operations: Operation[];
|
|
37
|
+
header: DocumentHeader;
|
|
38
|
+
}>
|
|
39
|
+
): Promise<void>;
|
|
40
|
+
|
|
41
|
+
abstract deleteDocument(drive: string, id: string): Promise<void>;
|
|
42
|
+
|
|
43
|
+
abstract getOperationResultingState?(
|
|
44
|
+
drive: string,
|
|
45
|
+
id: string,
|
|
46
|
+
index: number,
|
|
47
|
+
scope: string,
|
|
48
|
+
branch: string
|
|
49
|
+
): Promise<unknown>;
|
|
50
|
+
|
|
51
|
+
abstract setStorageDelegate?(delegate: IStorageDelegate): void;
|
|
52
|
+
|
|
53
|
+
abstract getSynchronizationUnitsRevision(
|
|
54
|
+
units: SynchronizationUnitQuery[]
|
|
55
|
+
): Promise<
|
|
56
|
+
{
|
|
57
|
+
driveId: string;
|
|
58
|
+
documentId: string;
|
|
59
|
+
scope: string;
|
|
60
|
+
branch: string;
|
|
61
|
+
lastUpdated: string;
|
|
62
|
+
revision: number;
|
|
63
|
+
}[]
|
|
64
|
+
>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export abstract class BaseDriveStorage
|
|
68
|
+
extends BaseStorage
|
|
69
|
+
implements IDriveStorage
|
|
70
|
+
{
|
|
71
|
+
abstract getDrives(): Promise<string[]>;
|
|
72
|
+
abstract getDrive(id: string): Promise<DocumentDriveStorage>;
|
|
73
|
+
abstract getDriveBySlug(slug: string): Promise<DocumentDriveStorage>;
|
|
74
|
+
abstract createDrive(
|
|
75
|
+
id: string,
|
|
76
|
+
drive: DocumentDriveStorage
|
|
77
|
+
): Promise<void>;
|
|
78
|
+
abstract deleteDrive(id: string): Promise<void>;
|
|
79
|
+
abstract addDriveOperations(
|
|
80
|
+
id: string,
|
|
81
|
+
operations: Operation<DocumentDriveAction | BaseAction>[],
|
|
82
|
+
header: DocumentHeader
|
|
83
|
+
): Promise<void>;
|
|
84
|
+
}
|
package/src/storage/index.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
-
BaseDocumentDriveServer,
|
|
3
2
|
DefaultRemoteDriveInfo,
|
|
4
3
|
DocumentDriveServerOptions,
|
|
5
4
|
DriveEvents,
|
|
5
|
+
IBaseDocumentDriveServer,
|
|
6
|
+
IReadModeDriveServer,
|
|
7
|
+
RemoteDriveAccessLevel,
|
|
6
8
|
RemoveOldRemoteDrivesOption
|
|
7
9
|
} from '../server';
|
|
8
10
|
import { DriveNotFoundError } from '../server/error';
|
|
@@ -13,20 +15,34 @@ export interface IServerDelegateDrivesManager {
|
|
|
13
15
|
emit: (...args: Parameters<DriveEvents['defaultRemoteDrive']>) => void;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
function isReadModeDriveServer(obj: unknown): obj is IReadModeDriveServer {
|
|
19
|
+
return typeof (obj as IReadModeDriveServer).getReadDrives === 'function';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface IDefaultDrivesManager {
|
|
23
|
+
getDefaultRemoteDrives(): Map<string, DefaultRemoteDriveInfo>;
|
|
24
|
+
setDefaultDriveAccessLevel(
|
|
25
|
+
url: string,
|
|
26
|
+
level: RemoteDriveAccessLevel
|
|
27
|
+
): Promise<void>;
|
|
28
|
+
setAllDefaultDrivesAccessLevel(
|
|
29
|
+
level: RemoteDriveAccessLevel
|
|
30
|
+
): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class DefaultDrivesManager implements IDefaultDrivesManager {
|
|
17
34
|
private defaultRemoteDrives = new Map<string, DefaultRemoteDriveInfo>();
|
|
18
35
|
private removeOldRemoteDrivesConfig: RemoveOldRemoteDrivesOption;
|
|
19
36
|
|
|
20
37
|
constructor(
|
|
21
|
-
private server:
|
|
38
|
+
private server:
|
|
39
|
+
| IBaseDocumentDriveServer
|
|
40
|
+
| (IBaseDocumentDriveServer & IReadModeDriveServer),
|
|
22
41
|
private delegate: IServerDelegateDrivesManager,
|
|
23
|
-
options?: Pick<
|
|
24
|
-
DocumentDriveServerOptions,
|
|
25
|
-
'defaultRemoteDrives' | 'removeOldRemoteDrives'
|
|
26
|
-
>
|
|
42
|
+
options?: Pick<DocumentDriveServerOptions, 'defaultDrives'>
|
|
27
43
|
) {
|
|
28
|
-
if (options?.
|
|
29
|
-
for (const defaultDrive of options.
|
|
44
|
+
if (options?.defaultDrives.remoteDrives) {
|
|
45
|
+
for (const defaultDrive of options.defaultDrives.remoteDrives) {
|
|
30
46
|
this.defaultRemoteDrives.set(defaultDrive.url, {
|
|
31
47
|
...defaultDrive,
|
|
32
48
|
status: 'PENDING'
|
|
@@ -34,13 +50,18 @@ export class DefaultDrivesManager {
|
|
|
34
50
|
}
|
|
35
51
|
}
|
|
36
52
|
|
|
37
|
-
this.removeOldRemoteDrivesConfig = options?.
|
|
53
|
+
this.removeOldRemoteDrivesConfig = options?.defaultDrives
|
|
54
|
+
.removeOldRemoteDrives || {
|
|
38
55
|
strategy: 'preserve-all'
|
|
39
56
|
};
|
|
40
57
|
}
|
|
41
58
|
|
|
42
59
|
getDefaultRemoteDrives() {
|
|
43
|
-
return
|
|
60
|
+
return new Map(
|
|
61
|
+
JSON.parse(
|
|
62
|
+
JSON.stringify(Array.from(this.defaultRemoteDrives))
|
|
63
|
+
) as Iterable<[string, DefaultRemoteDriveInfo]>
|
|
64
|
+
);
|
|
44
65
|
}
|
|
45
66
|
|
|
46
67
|
private async deleteDriveById(driveId: string) {
|
|
@@ -145,10 +166,40 @@ export class DefaultDrivesManager {
|
|
|
145
166
|
}
|
|
146
167
|
}
|
|
147
168
|
|
|
148
|
-
async
|
|
169
|
+
async setAllDefaultDrivesAccessLevel(level: RemoteDriveAccessLevel) {
|
|
170
|
+
const drives = this.defaultRemoteDrives.values();
|
|
171
|
+
for (const drive of drives) {
|
|
172
|
+
await this.setDefaultDriveAccessLevel(drive.url, level);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async setDefaultDriveAccessLevel(
|
|
177
|
+
url: string,
|
|
178
|
+
level: RemoteDriveAccessLevel
|
|
179
|
+
) {
|
|
180
|
+
const drive = this.defaultRemoteDrives.get(url);
|
|
181
|
+
if (drive && drive.options.accessLevel !== level) {
|
|
182
|
+
const newDriveValue = {
|
|
183
|
+
...drive,
|
|
184
|
+
options: { ...drive.options, accessLevel: level }
|
|
185
|
+
};
|
|
186
|
+
this.defaultRemoteDrives.set(url, newDriveValue);
|
|
187
|
+
await this.initializeDefaultRemoteDrives([newDriveValue]);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async initializeDefaultRemoteDrives(
|
|
192
|
+
defaultDrives: DefaultRemoteDriveInfo[] = Array.from(
|
|
193
|
+
this.defaultRemoteDrives.values()
|
|
194
|
+
)
|
|
195
|
+
) {
|
|
149
196
|
const drives = await this.server.getDrives();
|
|
197
|
+
const readServer = isReadModeDriveServer(this.server)
|
|
198
|
+
? (this.server as IReadModeDriveServer)
|
|
199
|
+
: undefined;
|
|
200
|
+
const readDrives = await readServer?.getReadDrives();
|
|
150
201
|
|
|
151
|
-
for (const remoteDrive of
|
|
202
|
+
for (const remoteDrive of defaultDrives) {
|
|
152
203
|
let remoteDriveInfo = { ...remoteDrive };
|
|
153
204
|
|
|
154
205
|
try {
|
|
@@ -158,7 +209,29 @@ export class DefaultDrivesManager {
|
|
|
158
209
|
|
|
159
210
|
this.defaultRemoteDrives.set(remoteDrive.url, remoteDriveInfo);
|
|
160
211
|
|
|
161
|
-
|
|
212
|
+
const driveIsAdded = drives.includes(driveInfo.id);
|
|
213
|
+
const readDriveIsAdded = readDrives?.includes(driveInfo.id);
|
|
214
|
+
|
|
215
|
+
const readMode =
|
|
216
|
+
readServer && remoteDrive.options.accessLevel === 'READ';
|
|
217
|
+
const isAdded = readMode ? readDriveIsAdded : driveIsAdded;
|
|
218
|
+
|
|
219
|
+
// if the read mode has changed then existing drives
|
|
220
|
+
// in the previous mode should be deleted
|
|
221
|
+
const driveToDelete = readMode
|
|
222
|
+
? driveIsAdded
|
|
223
|
+
: readDriveIsAdded;
|
|
224
|
+
if (driveToDelete) {
|
|
225
|
+
try {
|
|
226
|
+
await (readMode
|
|
227
|
+
? this.server.deleteDrive(driveInfo.id)
|
|
228
|
+
: readServer?.deleteReadDrive(driveInfo.id));
|
|
229
|
+
} catch (e) {
|
|
230
|
+
logger.error(e);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (isAdded) {
|
|
162
235
|
remoteDriveInfo.status = 'ALREADY_ADDED';
|
|
163
236
|
|
|
164
237
|
this.defaultRemoteDrives.set(
|
package/src/utils/graphql.ts
CHANGED
|
@@ -1,8 +1,60 @@
|
|
|
1
|
+
import { pascalCase } from 'change-case';
|
|
2
|
+
import {
|
|
3
|
+
DocumentDriveLocalState,
|
|
4
|
+
FileNode,
|
|
5
|
+
FolderNode
|
|
6
|
+
} from 'document-model-libs/document-drive';
|
|
7
|
+
import { Document, DocumentModel, Operation } from 'document-model/document';
|
|
8
|
+
import { DocumentModelState } from 'document-model/document-model';
|
|
9
|
+
import {
|
|
10
|
+
BuildSchemaOptions,
|
|
11
|
+
GraphQLError,
|
|
12
|
+
GraphQLList,
|
|
13
|
+
GraphQLNonNull,
|
|
14
|
+
GraphQLObjectType,
|
|
15
|
+
GraphQLOutputType,
|
|
16
|
+
GraphQLScalarType,
|
|
17
|
+
GraphQLUnionType,
|
|
18
|
+
ParseOptions,
|
|
19
|
+
buildSchema
|
|
20
|
+
} from 'graphql';
|
|
1
21
|
import request, { GraphQLClient, gql } from 'graphql-request';
|
|
22
|
+
import {
|
|
23
|
+
InferDocumentLocalState,
|
|
24
|
+
InferDocumentOperation,
|
|
25
|
+
InferDocumentState
|
|
26
|
+
} from '../read-mode/types';
|
|
2
27
|
import { logger } from './logger';
|
|
3
28
|
|
|
4
29
|
export { gql } from 'graphql-request';
|
|
5
30
|
|
|
31
|
+
type ReqGraphQLError = {
|
|
32
|
+
message: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type GraphQLResult<T> = { [K in keyof T]: T[K] | null } & {
|
|
36
|
+
errors?: GraphQLError[];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// replaces fetch so it can be used in Node and Browser envs
|
|
40
|
+
export async function requestGraphql<T>(
|
|
41
|
+
...args: Parameters<typeof request>
|
|
42
|
+
): Promise<GraphQLResult<T>> {
|
|
43
|
+
const [url, ...requestArgs] = args;
|
|
44
|
+
const client = new GraphQLClient(url, { fetch });
|
|
45
|
+
const { errors, ...response } = await client.request<
|
|
46
|
+
{ [K in keyof T]: T[K] | null } & { errors?: ReqGraphQLError[] }
|
|
47
|
+
>(...requestArgs);
|
|
48
|
+
|
|
49
|
+
const result = { ...response } as GraphQLResult<T>;
|
|
50
|
+
if (errors?.length) {
|
|
51
|
+
result.errors = errors.map(
|
|
52
|
+
({ message, ...options }) => new GraphQLError(message, options)
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
|
|
6
58
|
export type DriveInfo = {
|
|
7
59
|
id: string;
|
|
8
60
|
name: string;
|
|
@@ -10,11 +62,80 @@ export type DriveInfo = {
|
|
|
10
62
|
icon?: string;
|
|
11
63
|
};
|
|
12
64
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
65
|
+
function getFields(type: GraphQLOutputType): string {
|
|
66
|
+
if (type instanceof GraphQLObjectType) {
|
|
67
|
+
return Object.entries(type.getFields())
|
|
68
|
+
.map(([fieldName, field]) => {
|
|
69
|
+
const fieldType =
|
|
70
|
+
field.type instanceof GraphQLNonNull
|
|
71
|
+
? field.type.ofType
|
|
72
|
+
: field.type;
|
|
73
|
+
|
|
74
|
+
if (
|
|
75
|
+
fieldType instanceof GraphQLObjectType ||
|
|
76
|
+
fieldType instanceof GraphQLUnionType
|
|
77
|
+
) {
|
|
78
|
+
return `${fieldName} { ${getFields(fieldType)} }`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (fieldType instanceof GraphQLList) {
|
|
82
|
+
const listItemType =
|
|
83
|
+
fieldType.ofType instanceof GraphQLNonNull
|
|
84
|
+
? fieldType.ofType.ofType
|
|
85
|
+
: fieldType.ofType;
|
|
86
|
+
|
|
87
|
+
if (listItemType instanceof GraphQLScalarType) {
|
|
88
|
+
return fieldName;
|
|
89
|
+
} else if (
|
|
90
|
+
listItemType instanceof GraphQLObjectType ||
|
|
91
|
+
listItemType instanceof GraphQLUnionType
|
|
92
|
+
) {
|
|
93
|
+
return `${fieldName} { ${getFields(listItemType)} }`;
|
|
94
|
+
} else {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`List item type ${listItemType.toString()} is not handled`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return fieldName;
|
|
102
|
+
})
|
|
103
|
+
.join(' ');
|
|
104
|
+
} else if (type instanceof GraphQLUnionType) {
|
|
105
|
+
return type
|
|
106
|
+
.getTypes()
|
|
107
|
+
.map(unionType => {
|
|
108
|
+
return `... on ${unionType.name} { ${getFields(unionType)} }`;
|
|
109
|
+
})
|
|
110
|
+
.join(' ');
|
|
111
|
+
}
|
|
112
|
+
return '';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function generateDocumentStateQueryFields(
|
|
116
|
+
documentModel: DocumentModelState,
|
|
117
|
+
options?: BuildSchemaOptions & ParseOptions
|
|
118
|
+
): string {
|
|
119
|
+
const name = pascalCase(documentModel.name);
|
|
120
|
+
const spec = documentModel.specifications.at(-1);
|
|
121
|
+
if (!spec) {
|
|
122
|
+
throw new Error('No document model specification found');
|
|
123
|
+
}
|
|
124
|
+
const source = `${spec.state.global.schema} type Query { ${name}: ${name}State }`;
|
|
125
|
+
const schema = buildSchema(source, options);
|
|
126
|
+
const queryType = schema.getQueryType();
|
|
127
|
+
if (!queryType) {
|
|
128
|
+
throw new Error('No query type found');
|
|
129
|
+
}
|
|
130
|
+
const fields = queryType.getFields();
|
|
131
|
+
const stateQuery = fields[name];
|
|
132
|
+
|
|
133
|
+
if (!stateQuery) {
|
|
134
|
+
throw new Error('No state query found');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const queryFields = getFields(stateQuery.type);
|
|
138
|
+
return queryFields;
|
|
18
139
|
}
|
|
19
140
|
|
|
20
141
|
export async function requestPublicDrive(url: string): Promise<DriveInfo> {
|
|
@@ -33,15 +154,135 @@ export async function requestPublicDrive(url: string): Promise<DriveInfo> {
|
|
|
33
154
|
}
|
|
34
155
|
`
|
|
35
156
|
);
|
|
157
|
+
if (result.errors?.length || !result.drive) {
|
|
158
|
+
throw result.errors?.at(0) ?? new Error('Drive not found');
|
|
159
|
+
}
|
|
36
160
|
drive = result.drive;
|
|
37
161
|
} catch (e) {
|
|
38
162
|
logger.error(e);
|
|
39
163
|
throw new Error("Couldn't find drive info");
|
|
40
164
|
}
|
|
41
165
|
|
|
42
|
-
if (!drive) {
|
|
43
|
-
throw new Error('Drive not found');
|
|
44
|
-
}
|
|
45
|
-
|
|
46
166
|
return drive;
|
|
47
167
|
}
|
|
168
|
+
|
|
169
|
+
export type DriveState = DriveInfo &
|
|
170
|
+
Pick<DocumentDriveLocalState, 'availableOffline' | 'sharingType'> & {
|
|
171
|
+
nodes: Array<FolderNode | Omit<FileNode, 'synchronizationUnits'>>;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export async function fetchDocument<D extends Document>(
|
|
175
|
+
url: string,
|
|
176
|
+
documentId: string,
|
|
177
|
+
documentModelLib: DocumentModel<
|
|
178
|
+
InferDocumentState<D>,
|
|
179
|
+
InferDocumentOperation<D>,
|
|
180
|
+
InferDocumentLocalState<D>
|
|
181
|
+
>
|
|
182
|
+
): Promise<
|
|
183
|
+
GraphQLResult<{
|
|
184
|
+
document: Document<
|
|
185
|
+
InferDocumentState<D>,
|
|
186
|
+
InferDocumentOperation<D>,
|
|
187
|
+
InferDocumentLocalState<D>
|
|
188
|
+
>;
|
|
189
|
+
}>
|
|
190
|
+
> {
|
|
191
|
+
const { documentModel, utils } = documentModelLib;
|
|
192
|
+
const stateFields = generateDocumentStateQueryFields(documentModel);
|
|
193
|
+
const name = pascalCase(documentModel.name);
|
|
194
|
+
const result = await requestGraphql<{
|
|
195
|
+
document: Pick<
|
|
196
|
+
D,
|
|
197
|
+
'name' | 'created' | 'documentType' | 'lastModified'
|
|
198
|
+
> & {
|
|
199
|
+
id: string;
|
|
200
|
+
revision: number;
|
|
201
|
+
state: InferDocumentState<D>;
|
|
202
|
+
initialState: InferDocumentState<D>;
|
|
203
|
+
operations: (Pick<
|
|
204
|
+
Operation,
|
|
205
|
+
| 'id'
|
|
206
|
+
| 'hash'
|
|
207
|
+
| 'index'
|
|
208
|
+
| 'skip'
|
|
209
|
+
| 'timestamp'
|
|
210
|
+
| 'type'
|
|
211
|
+
| 'error'
|
|
212
|
+
> & { inputText: string })[];
|
|
213
|
+
};
|
|
214
|
+
}>(
|
|
215
|
+
url,
|
|
216
|
+
gql`
|
|
217
|
+
query ($id: String!) {
|
|
218
|
+
document(id: $id) {
|
|
219
|
+
id
|
|
220
|
+
name
|
|
221
|
+
created
|
|
222
|
+
documentType
|
|
223
|
+
lastModified
|
|
224
|
+
revision
|
|
225
|
+
operations {
|
|
226
|
+
id
|
|
227
|
+
error
|
|
228
|
+
hash
|
|
229
|
+
index
|
|
230
|
+
skip
|
|
231
|
+
timestamp
|
|
232
|
+
type
|
|
233
|
+
inputText
|
|
234
|
+
}
|
|
235
|
+
... on ${name} {
|
|
236
|
+
state {
|
|
237
|
+
${stateFields}
|
|
238
|
+
}
|
|
239
|
+
initialState {
|
|
240
|
+
${stateFields}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
`,
|
|
246
|
+
{ id: documentId }
|
|
247
|
+
);
|
|
248
|
+
const document: Document<
|
|
249
|
+
InferDocumentState<D>,
|
|
250
|
+
InferDocumentOperation<D>,
|
|
251
|
+
InferDocumentLocalState<D>
|
|
252
|
+
> | null = result.document
|
|
253
|
+
? {
|
|
254
|
+
...result.document,
|
|
255
|
+
revision: {
|
|
256
|
+
global: result.document.revision,
|
|
257
|
+
local: 0
|
|
258
|
+
},
|
|
259
|
+
state: utils.createState({ global: result.document.state }),
|
|
260
|
+
operations: {
|
|
261
|
+
global: result.document.operations.map(
|
|
262
|
+
({ inputText, ...o }) => ({
|
|
263
|
+
...o,
|
|
264
|
+
error: o.error ?? undefined,
|
|
265
|
+
scope: 'global',
|
|
266
|
+
input: JSON.parse(inputText) as D
|
|
267
|
+
})
|
|
268
|
+
),
|
|
269
|
+
local: []
|
|
270
|
+
},
|
|
271
|
+
attachments: {},
|
|
272
|
+
initialState: utils.createExtendedState({
|
|
273
|
+
// TODO: getDocument should return all the initial state fields
|
|
274
|
+
created: result.document.created,
|
|
275
|
+
lastModified: result.document.created,
|
|
276
|
+
state: utils.createState({
|
|
277
|
+
global: result.document.initialState
|
|
278
|
+
})
|
|
279
|
+
}),
|
|
280
|
+
clipboard: []
|
|
281
|
+
}
|
|
282
|
+
: null;
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
...result,
|
|
286
|
+
document
|
|
287
|
+
};
|
|
288
|
+
}
|