document-drive 1.0.0-alpha.91 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "document-drive",
3
- "version": "1.0.0-alpha.91",
3
+ "version": "1.0.0-alpha.93",
4
4
  "license": "AGPL-3.0-only",
5
5
  "type": "module",
6
6
  "module": "./src/index.ts",
@@ -35,7 +35,7 @@
35
35
  "test:watch": "vitest watch"
36
36
  },
37
37
  "peerDependencies": {
38
- "document-model": "^1.7.0",
38
+ "document-model": "^1.8.0",
39
39
  "document-model-libs": "^1.57.0"
40
40
  },
41
41
  "optionalDependencies": {
@@ -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",
@@ -65,9 +66,10 @@
65
66
  "@types/uuid": "^9.0.8",
66
67
  "@typescript-eslint/eslint-plugin": "^6.21.0",
67
68
  "@typescript-eslint/parser": "^6.21.0",
69
+ "@vitest/browser": "^2.0.5",
68
70
  "@vitest/coverage-v8": "^2.0.5",
69
71
  "document-model": "^1.7.0",
70
- "document-model-libs": "^1.70.0",
72
+ "document-model-libs": "^1.83.0",
71
73
  "eslint": "^8.57.0",
72
74
  "eslint-config-prettier": "^9.1.0",
73
75
  "fake-indexeddb": "^5.0.2",
@@ -80,7 +82,9 @@
80
82
  "sequelize": "^6.37.2",
81
83
  "sqlite3": "^5.1.7",
82
84
  "typescript": "^5.5.3",
83
- "vitest": "^2.0.5"
85
+ "vitest": "^2.0.5",
86
+ "webdriverio": "^9.0.9",
87
+ "vitest-fetch-mock": "^0.3.0"
84
88
  },
85
89
  "packageManager": "pnpm@9.1.4+sha256.30a1801ac4e723779efed13a21f4c39f9eb6c9fbb4ced101bce06b422593d7c9"
86
90
  }
package/src/queue/base.ts CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  } from 'document-model-libs/document-drive';
5
5
  import { Action } from 'document-model/document';
6
6
  import { Unsubscribe, createNanoEvents } from 'nanoevents';
7
- import { generateUUID } from '../utils';
7
+ import { generateUUID, runAsap } from '../utils';
8
8
  import { logger } from '../utils/logger';
9
9
  import {
10
10
  IJob,
@@ -212,10 +212,10 @@ export class BaseQueueManager implements IQueueManager {
212
212
  private retryNextJob(timeout?: number) {
213
213
  const _timeout = timeout !== undefined ? timeout : this.timeout;
214
214
  const retry =
215
- _timeout === 0 && typeof setImmediate !== 'undefined'
216
- ? setImmediate
217
- : (fn: () => void) => setTimeout(fn, _timeout);
218
- return retry(() => this.processNextJob());
215
+ _timeout > 0
216
+ ? (fn: () => void) => setTimeout(fn, _timeout)
217
+ : runAsap;
218
+ retry(() => this.processNextJob());
219
219
  }
220
220
 
221
221
  private async findFirstNonEmptyQueue(
@@ -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;
@@ -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;