@aerogel/plugin-local-first 0.0.0 → 0.1.0
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/aerogel-plugin-local-first.d.ts +48 -17
- package/dist/aerogel-plugin-local-first.js +1219 -1232
- package/dist/aerogel-plugin-local-first.js.map +1 -1
- package/package.json +5 -5
- package/src/components/Account.vue +9 -0
- package/src/components/AccountLoggedIn.vue +85 -0
- package/src/components/AccountLoggedOut.vue +26 -0
- package/src/components/AccountLoginModal.vue +36 -0
- package/src/components/AccountModal.vue +206 -0
- package/src/components/index.ts +5 -3
- package/src/components/settings/PurgeData.vue +43 -0
- package/src/components/settings/index.ts +10 -0
- package/src/index.css +1 -0
- package/src/index.ts +5 -1
- package/src/jobs/Migrate.test.ts +1 -1
- package/src/jobs/Migrate.ts +8 -1
- package/src/jobs/Sync.test.ts +313 -35
- package/src/jobs/Sync.ts +183 -77
- package/src/jobs/mixins/LoadsChildren.ts +48 -31
- package/src/jobs/mixins/LoadsTypeIndex.ts +7 -5
- package/src/jobs/mixins/TracksLocalModels.ts +4 -4
- package/src/lib/mirroring.ts +45 -21
- package/src/services/Cloud.ts +84 -25
- package/src/services/DocumentsCache.ts +6 -2
- package/src/testing/cloud.ts +1 -1
- package/src/testing/setup.ts +3 -2
- package/src/testing/stubs/Person.schema.ts +9 -0
- package/src/testing/stubs/Person.ts +18 -0
- package/src/testing/stubs/Post.schema.ts +17 -0
- package/src/testing/stubs/Post.ts +18 -0
- package/dist/InruptAuthenticator.lazy-BvpQLGoj-BvpQLGoj.js +0 -9
- package/dist/InruptAuthenticator.lazy-BvpQLGoj-BvpQLGoj.js.map +0 -1
- package/dist/LegacyAuthenticator.lazy-ChFFqP14-ChFFqP14.js +0 -5
- package/dist/LegacyAuthenticator.lazy-ChFFqP14-ChFFqP14.js.map +0 -1
- package/src/components/AGCloudStatus.vue +0 -31
package/src/lib/mirroring.ts
CHANGED
|
@@ -5,18 +5,18 @@ import {
|
|
|
5
5
|
requireUrlParentDirectory,
|
|
6
6
|
required,
|
|
7
7
|
tap,
|
|
8
|
-
toString,
|
|
9
8
|
urlResolveDirectory,
|
|
10
9
|
urlRoute,
|
|
11
10
|
} from '@noeldemartin/utils';
|
|
12
11
|
import { App } from '@aerogel/core';
|
|
13
12
|
import { getTrackedModels, trackModelsCollection as trackSoukaiModelsCollection } from '@aerogel/plugin-soukai';
|
|
14
13
|
import { Solid } from '@aerogel/plugin-solid';
|
|
15
|
-
import { PropertyOperation, SolidContainer, SolidContainsRelation, Tombstone } from 'soukai-solid';
|
|
14
|
+
import { PropertyOperation, SolidContainer, SolidContainsRelation, Tombstone, isContainerClass } from 'soukai-solid';
|
|
16
15
|
import type { ObjectsMap } from '@noeldemartin/utils';
|
|
17
16
|
import type { Attributes } from 'soukai';
|
|
18
|
-
import type { SolidModel, SolidModelConstructor, SolidSchemaDefinition } from 'soukai-solid';
|
|
17
|
+
import type { Operation, SolidModel, SolidModelConstructor, SolidSchemaDefinition } from 'soukai-solid';
|
|
19
18
|
|
|
19
|
+
import DocumentsCache from '@aerogel/plugin-local-first/services/DocumentsCache';
|
|
20
20
|
import SyncQueue from '@aerogel/plugin-local-first/lib/SyncQueue';
|
|
21
21
|
import { getContainerRelations, getRelatedAppModels } from '@aerogel/plugin-local-first/lib/inference';
|
|
22
22
|
|
|
@@ -88,6 +88,10 @@ export async function trackModelsCollection(modelClass: SolidModelConstructor, c
|
|
|
88
88
|
[modelClass.modelName]: (Cloud.modelCollections[modelClass.modelName] ?? []).concat(collection),
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
+
if (!Cloud.modelCollections[modelClass.modelName]?.includes(modelClass.collection)) {
|
|
92
|
+
modelClass.collection = collection;
|
|
93
|
+
}
|
|
94
|
+
|
|
91
95
|
await trackSoukaiModelsCollection(modelClass, collection);
|
|
92
96
|
}
|
|
93
97
|
|
|
@@ -107,24 +111,24 @@ export async function updateRemoteModel(localModel: SolidModel): Promise<void> {
|
|
|
107
111
|
},
|
|
108
112
|
});
|
|
109
113
|
|
|
114
|
+
await DocumentsCache.forget(localModel.requireDocumentUrl());
|
|
115
|
+
|
|
110
116
|
if (Cloud.autoPush && Cloud.ready && !Cloud.syncing) {
|
|
111
117
|
SyncQueue.push(localModel);
|
|
112
118
|
}
|
|
113
119
|
}
|
|
114
120
|
|
|
115
|
-
export function clearLocalModelUpdates(
|
|
116
|
-
const syncedModelUrls = models?.map((model) => toString(model.getDeletedPrimaryKey() ?? model.getPrimaryKey()));
|
|
121
|
+
export function clearLocalModelUpdates(syncedModelUrls: Set<string>, documentsWithErrors?: Set<string>): void {
|
|
117
122
|
const localModelUpdates = {} as Record<string, number>;
|
|
123
|
+
const syncedModelUrlsArray = Array.from(syncedModelUrls);
|
|
118
124
|
|
|
119
125
|
for (const [url, count] of Object.entries(Cloud.localModelUpdates)) {
|
|
120
126
|
const documentUrl = urlRoute(url);
|
|
121
127
|
const wasSynced =
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
(modelUrl) => url === modelUrl || (modelUrl.endsWith('/') && url.startsWith(modelUrl)),
|
|
125
|
-
);
|
|
128
|
+
syncedModelUrls.has(url) ||
|
|
129
|
+
syncedModelUrlsArray.some((modelUrl) => modelUrl.endsWith('/') && url.startsWith(modelUrl));
|
|
126
130
|
|
|
127
|
-
if (wasSynced && !
|
|
131
|
+
if (wasSynced && !documentsWithErrors?.has(documentUrl)) {
|
|
128
132
|
continue;
|
|
129
133
|
}
|
|
130
134
|
|
|
@@ -177,24 +181,27 @@ export function cloneRemoteModel(remoteModel: SolidModel): SolidModel {
|
|
|
177
181
|
export async function completeRemoteModels(
|
|
178
182
|
localModels: SolidModel[],
|
|
179
183
|
remoteModels: SolidModel[],
|
|
180
|
-
|
|
184
|
+
documentsWithErrors: Set<string>,
|
|
181
185
|
): Promise<SolidModel[]> {
|
|
182
186
|
const RemoteTombstone = getRemoteClass(Tombstone);
|
|
183
187
|
const remoteModelUrls = remoteModels.map((remoteModel) => remoteModel.url);
|
|
184
188
|
const missingModelDocumentUrls = localModels
|
|
185
189
|
.filter((localModel) => !remoteModelUrls.includes(localModel.url))
|
|
186
190
|
.map((localModel) => localModel.requireDocumentUrl())
|
|
187
|
-
.filter((documentUrl) => !
|
|
191
|
+
.filter((documentUrl) => !documentsWithErrors.has(documentUrl));
|
|
188
192
|
const tombstones = await RemoteTombstone.all({ $in: missingModelDocumentUrls });
|
|
189
193
|
|
|
190
194
|
return remoteModels.concat(tombstones);
|
|
191
195
|
}
|
|
192
196
|
|
|
193
197
|
export function ignoreModelUpdates(localModel: SolidModel): boolean {
|
|
194
|
-
const
|
|
198
|
+
const documentModels = localModel.getDocumentModels();
|
|
199
|
+
const documentOperations = documentModels.flatMap(
|
|
200
|
+
(model) => model.operations?.map((operation) => [model, operation] as [SolidModel, Operation]) ?? [],
|
|
201
|
+
);
|
|
195
202
|
|
|
196
|
-
return !
|
|
197
|
-
if (operation.date.getTime() !== updatedAt) {
|
|
203
|
+
return !documentOperations.some(([model, operation]) => {
|
|
204
|
+
if (operation.date.getTime() !== model.metadata?.updatedAt?.getTime()) {
|
|
198
205
|
return false;
|
|
199
206
|
}
|
|
200
207
|
|
|
@@ -202,7 +209,7 @@ export function ignoreModelUpdates(localModel: SolidModel): boolean {
|
|
|
202
209
|
return true;
|
|
203
210
|
}
|
|
204
211
|
|
|
205
|
-
return !
|
|
212
|
+
return !model.ignoreRdfPropertyHistory(operation.property, true);
|
|
206
213
|
});
|
|
207
214
|
}
|
|
208
215
|
|
|
@@ -211,7 +218,9 @@ export function getLocalModel(remoteModel: SolidModel, localModels: ObjectsMap<S
|
|
|
211
218
|
}
|
|
212
219
|
|
|
213
220
|
export function getLocalModels(): SolidModel[] {
|
|
214
|
-
return [...Cloud.registeredModels]
|
|
221
|
+
return [...Cloud.registeredModels]
|
|
222
|
+
.map(({ modelClass }) => getTrackedModels(modelClass, { includeSoftDeleted: true }))
|
|
223
|
+
.flat();
|
|
215
224
|
}
|
|
216
225
|
|
|
217
226
|
export function getSchemaMigrations(): Map<SolidModelConstructor, SolidModelConstructor | SolidSchemaDefinition> {
|
|
@@ -232,12 +241,27 @@ export function getRemoteClass<T extends SolidModelConstructor>(localClass: T):
|
|
|
232
241
|
return (remoteClasses.get(localClass) as T) ?? makeRemoteClass(localClass);
|
|
233
242
|
}
|
|
234
243
|
|
|
235
|
-
export function getRemoteContainersCollection(path?: string): string {
|
|
244
|
+
export function getRemoteContainersCollection(modelClass: SolidModelConstructor, path?: string): string {
|
|
236
245
|
const rootStorage = Solid.requireUser().storageUrls[0];
|
|
246
|
+
const containedClass =
|
|
247
|
+
isContainerClass(modelClass) &&
|
|
248
|
+
getContainerRelations(modelClass)
|
|
249
|
+
.map((relation) => {
|
|
250
|
+
const relatedClass = modelClass
|
|
251
|
+
.instance()
|
|
252
|
+
.requireRelation<SolidContainsRelation>(relation).relatedClass;
|
|
253
|
+
|
|
254
|
+
if (isContainerClass(relatedClass)) {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return relatedClass;
|
|
259
|
+
})
|
|
260
|
+
.filter(Boolean)[0];
|
|
261
|
+
|
|
262
|
+
path ??= `/${(containedClass || modelClass).modelName.toLowerCase()}s/`;
|
|
237
263
|
|
|
238
|
-
return path
|
|
239
|
-
? urlResolveDirectory((path.startsWith('/') ? rootStorage.slice(0, -1) : rootStorage) + path)
|
|
240
|
-
: rootStorage;
|
|
264
|
+
return urlResolveDirectory((path.startsWith('/') ? rootStorage.slice(0, -1) : rootStorage) + path);
|
|
241
265
|
}
|
|
242
266
|
|
|
243
267
|
export function getRemoteModel(localModel: SolidModel, remoteModels: ObjectsMap<SolidModel>): SolidModel {
|
package/src/services/Cloud.ts
CHANGED
|
@@ -6,13 +6,15 @@ import {
|
|
|
6
6
|
getLocationQueryParameter,
|
|
7
7
|
hasLocationQueryParameter,
|
|
8
8
|
isArray,
|
|
9
|
+
isTesting,
|
|
9
10
|
parseBoolean,
|
|
10
11
|
} from '@noeldemartin/utils';
|
|
11
|
-
import {
|
|
12
|
+
import { Errors, Events, dispatch, translateWithDefault } from '@aerogel/core';
|
|
12
13
|
import { Solid } from '@aerogel/plugin-solid';
|
|
13
|
-
import { SolidModel, Tombstone, isContainer, isContainerClass } from 'soukai-solid';
|
|
14
|
+
import { SolidContainer, SolidModel, Tombstone, isContainer, isContainerClass, isSolidModel } from 'soukai-solid';
|
|
14
15
|
import { getTrackedModels, trackModels, trackModelsCollection } from '@aerogel/plugin-soukai';
|
|
15
16
|
import { watchEffect } from 'vue';
|
|
17
|
+
import { getBootedModels } from 'soukai';
|
|
16
18
|
import type { Authenticator } from '@aerogel/plugin-solid';
|
|
17
19
|
import type { Engine } from 'soukai';
|
|
18
20
|
import type { JobListener } from '@aerogel/core';
|
|
@@ -27,6 +29,7 @@ import {
|
|
|
27
29
|
updateRemoteModel,
|
|
28
30
|
} from '@aerogel/plugin-local-first/lib/mirroring';
|
|
29
31
|
import Backup from '@aerogel/plugin-local-first/jobs/Backup';
|
|
32
|
+
import DocumentsCache from '@aerogel/plugin-local-first/services/DocumentsCache';
|
|
30
33
|
import Migrate from '@aerogel/plugin-local-first/jobs/Migrate';
|
|
31
34
|
import Sync from '@aerogel/plugin-local-first/jobs/Sync';
|
|
32
35
|
import SyncQueue from '@aerogel/plugin-local-first/lib/SyncQueue';
|
|
@@ -43,9 +46,10 @@ export interface RegisterOptions {
|
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
export interface SyncOptions extends JobListener {
|
|
46
|
-
refreshUserProfile?: boolean;
|
|
47
49
|
model?: SolidModel;
|
|
48
50
|
models?: SolidModel[];
|
|
51
|
+
refreshUserProfile?: boolean;
|
|
52
|
+
syncDirty?: boolean;
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
export class CloudService extends Service {
|
|
@@ -116,8 +120,8 @@ export class CloudService extends Service {
|
|
|
116
120
|
}
|
|
117
121
|
|
|
118
122
|
await this.asyncLock.run(async () => {
|
|
123
|
+
let models = options.model ? [options.model] : options.models;
|
|
119
124
|
const start = Date.now();
|
|
120
|
-
const models = options.model ? [options.model] : options.models;
|
|
121
125
|
|
|
122
126
|
this.syncError = null;
|
|
123
127
|
this.status = CloudStatus.Syncing;
|
|
@@ -131,23 +135,33 @@ export class CloudService extends Service {
|
|
|
131
135
|
await Solid.refreshUserProfile();
|
|
132
136
|
}
|
|
133
137
|
|
|
134
|
-
|
|
138
|
+
const syncDirty = options.syncDirty ?? true;
|
|
139
|
+
let syncs = 0;
|
|
135
140
|
|
|
136
|
-
|
|
141
|
+
do {
|
|
142
|
+
this.syncJob = new Sync(models);
|
|
137
143
|
|
|
138
|
-
|
|
144
|
+
this.syncJob.listeners.add(options);
|
|
139
145
|
|
|
140
|
-
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
146
|
+
await dispatch(this.syncJob);
|
|
143
147
|
|
|
144
|
-
|
|
148
|
+
if (this.syncJob.cancelled) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
syncs++;
|
|
153
|
+
models = this.getDirtyLocalModels().filter(
|
|
154
|
+
(model) => !this.syncJob?.documentsWithErrors.has(model.requireDocumentUrl()),
|
|
155
|
+
);
|
|
156
|
+
} while (syncDirty && models.length > 0 && syncs < 3);
|
|
157
|
+
|
|
158
|
+
if (!isTesting('unit')) {
|
|
145
159
|
await after({ milliseconds: Math.max(500, 1000 - (Date.now() - start)) });
|
|
146
160
|
}
|
|
147
161
|
|
|
148
162
|
this.syncJob = null;
|
|
149
163
|
} catch (error) {
|
|
150
|
-
if (
|
|
164
|
+
if (isTesting()) {
|
|
151
165
|
throw error;
|
|
152
166
|
}
|
|
153
167
|
|
|
@@ -226,17 +240,18 @@ export class CloudService extends Service {
|
|
|
226
240
|
}
|
|
227
241
|
|
|
228
242
|
this.whenReady(() => {
|
|
229
|
-
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
243
|
+
const [existingCollection] = this.modelCollections[modelClass.modelName] ?? [];
|
|
232
244
|
|
|
233
|
-
modelClass.
|
|
245
|
+
modelClass.useSoftDeletes(true);
|
|
246
|
+
|
|
247
|
+
modelClass.collection = existingCollection ?? getRemoteContainersCollection(modelClass, options.path);
|
|
234
248
|
});
|
|
235
249
|
|
|
236
250
|
await trackModels(modelClass, {
|
|
237
|
-
created: (model) => createRemoteModel(model),
|
|
238
|
-
updated: (model) => updateRemoteModel(model),
|
|
239
|
-
deleted: (model) =>
|
|
251
|
+
created: (model) => this.ready && createRemoteModel(model),
|
|
252
|
+
updated: (model) => this.ready && updateRemoteModel(model),
|
|
253
|
+
deleted: (model) =>
|
|
254
|
+
this.ready && clearLocalModelUpdates(new Set([model.url ?? model.getDeletedPrimaryKey()])),
|
|
240
255
|
});
|
|
241
256
|
|
|
242
257
|
if (isContainerClass(modelClass)) {
|
|
@@ -245,9 +260,13 @@ export class CloudService extends Service {
|
|
|
245
260
|
.instance()
|
|
246
261
|
.requireRelation<SolidContainsRelation>(relation).relatedClass;
|
|
247
262
|
|
|
248
|
-
relatedClass.on('created', (model) => createRemoteModel(model));
|
|
249
|
-
relatedClass.on('updated', (model) => updateRemoteModel(model));
|
|
250
|
-
relatedClass.on(
|
|
263
|
+
relatedClass.on('created', (model) => this.ready && createRemoteModel(model));
|
|
264
|
+
relatedClass.on('updated', (model) => this.ready && updateRemoteModel(model));
|
|
265
|
+
relatedClass.on(
|
|
266
|
+
'deleted',
|
|
267
|
+
(model) =>
|
|
268
|
+
this.ready && clearLocalModelUpdates(new Set([model.url ?? model.getDeletedPrimaryKey()])),
|
|
269
|
+
);
|
|
251
270
|
});
|
|
252
271
|
}
|
|
253
272
|
|
|
@@ -271,7 +290,7 @@ export class CloudService extends Service {
|
|
|
271
290
|
continue;
|
|
272
291
|
}
|
|
273
292
|
|
|
274
|
-
return getRemoteContainersCollection(registration.path);
|
|
293
|
+
return getRemoteContainersCollection(registration.modelClass, registration.path);
|
|
275
294
|
}
|
|
276
295
|
|
|
277
296
|
throw new Error(`Failed resolving remote collection for '${modelClass.modelName}' model`);
|
|
@@ -298,6 +317,9 @@ export class CloudService extends Service {
|
|
|
298
317
|
|
|
299
318
|
this.pollingInterval = setInterval(() => this.sync(), this.pollingMinutes * 60 * 1000);
|
|
300
319
|
});
|
|
320
|
+
|
|
321
|
+
await this.bustDocumentsCache();
|
|
322
|
+
await this.registerModels();
|
|
301
323
|
}
|
|
302
324
|
|
|
303
325
|
protected getDirtyLocalModels(): SolidModel[] {
|
|
@@ -305,7 +327,7 @@ export class CloudService extends Service {
|
|
|
305
327
|
const dirtyLocalModels = [];
|
|
306
328
|
|
|
307
329
|
for (const { modelClass } of this.registeredModels) {
|
|
308
|
-
for (const model of getTrackedModels(modelClass)) {
|
|
330
|
+
for (const model of getTrackedModels(modelClass, { includeSoftDeleted: true })) {
|
|
309
331
|
dirtyLocalModels.push(...this.getModelsIn(urls, model));
|
|
310
332
|
}
|
|
311
333
|
}
|
|
@@ -362,7 +384,7 @@ export class CloudService extends Service {
|
|
|
362
384
|
}
|
|
363
385
|
|
|
364
386
|
const localCollection = modelClass.collection;
|
|
365
|
-
const remoteCollection = getRemoteContainersCollection(path);
|
|
387
|
+
const remoteCollection = getRemoteContainersCollection(modelClass, path);
|
|
366
388
|
|
|
367
389
|
modelClass.collection = remoteCollection;
|
|
368
390
|
|
|
@@ -398,6 +420,8 @@ export class CloudService extends Service {
|
|
|
398
420
|
this.status = CloudStatus.Online;
|
|
399
421
|
this.engine = authenticator.engine;
|
|
400
422
|
|
|
423
|
+
getRemoteClass(SolidContainer).setEngine(this.engine);
|
|
424
|
+
|
|
401
425
|
for (const relatedClass of relatedClasses) {
|
|
402
426
|
getRemoteClass(relatedClass).setEngine(this.engine);
|
|
403
427
|
}
|
|
@@ -405,6 +429,7 @@ export class CloudService extends Service {
|
|
|
405
429
|
|
|
406
430
|
protected logout(): void {
|
|
407
431
|
this.setState({
|
|
432
|
+
autoPush: true,
|
|
408
433
|
localModelUpdates: {},
|
|
409
434
|
migrationDismissed: false,
|
|
410
435
|
migrationJob: null,
|
|
@@ -413,10 +438,13 @@ export class CloudService extends Service {
|
|
|
413
438
|
pollingEnabled: true,
|
|
414
439
|
pollingMinutes: 10,
|
|
415
440
|
ready: false,
|
|
441
|
+
schemaMigrations: new Map(),
|
|
416
442
|
setupDismissed: false,
|
|
417
443
|
setupOngoing: false,
|
|
418
444
|
startupSync: true,
|
|
419
445
|
status: CloudStatus.Disconnected,
|
|
446
|
+
syncError: null,
|
|
447
|
+
syncJob: null,
|
|
420
448
|
});
|
|
421
449
|
}
|
|
422
450
|
|
|
@@ -432,6 +460,30 @@ export class CloudService extends Service {
|
|
|
432
460
|
return this.startupSync;
|
|
433
461
|
}
|
|
434
462
|
|
|
463
|
+
private async bustDocumentsCache(): Promise<void> {
|
|
464
|
+
if (
|
|
465
|
+
!hasLocationQueryParameter('bustDocumentsCache') ||
|
|
466
|
+
!parseBoolean(getLocationQueryParameter('bustDocumentsCache'))
|
|
467
|
+
) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
await DocumentsCache.booted;
|
|
472
|
+
await DocumentsCache.clear();
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
private async registerModels(): Promise<void> {
|
|
476
|
+
const models = getBootedModels();
|
|
477
|
+
|
|
478
|
+
for (const model of models.values()) {
|
|
479
|
+
if (!isSolidModel(model) || !model.cloud) {
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
await this.register(model, typeof model.cloud === 'boolean' ? {} : model.cloud);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
435
487
|
}
|
|
436
488
|
|
|
437
489
|
export default facade(CloudService);
|
|
@@ -448,3 +500,10 @@ declare module '@aerogel/core' {
|
|
|
448
500
|
'cloud:sync-completed': SolidModel[] | undefined;
|
|
449
501
|
}
|
|
450
502
|
}
|
|
503
|
+
|
|
504
|
+
declare module 'soukai-solid' {
|
|
505
|
+
// eslint-disable-next-line @typescript-eslint/no-shadow
|
|
506
|
+
namespace SolidModel {
|
|
507
|
+
export const cloud: boolean | RegisterOptions | undefined;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { Events, Service } from '@aerogel/core';
|
|
1
|
+
import { Events, Service, appNamespace } from '@aerogel/core';
|
|
2
2
|
import { IndexedDBMap, facade } from '@noeldemartin/utils';
|
|
3
3
|
|
|
4
4
|
export class DocumentsCacheService extends Service {
|
|
5
5
|
|
|
6
|
-
private cache = new IndexedDBMap<{ modifiedAt: number }>(
|
|
6
|
+
private cache = new IndexedDBMap<{ modifiedAt: number }>(`${appNamespace()}-documents-cache`);
|
|
7
7
|
|
|
8
8
|
public async isFresh(documentUrl: string, modifiedAt: Date | number): Promise<boolean> {
|
|
9
9
|
const meta = await this.cache.get(documentUrl);
|
|
@@ -22,6 +22,10 @@ export class DocumentsCacheService extends Service {
|
|
|
22
22
|
await this.cache.delete(documentUrl);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
public async clear(): Promise<void> {
|
|
26
|
+
await this.cache.clear();
|
|
27
|
+
}
|
|
28
|
+
|
|
25
29
|
protected override async boot(): Promise<void> {
|
|
26
30
|
Events.on('auth:logout', async () => {
|
|
27
31
|
await this.cache.clear();
|
package/src/testing/cloud.ts
CHANGED
|
@@ -128,7 +128,7 @@ export function containerResponse(
|
|
|
128
128
|
`);
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
export function movieResponse(title: string): Response {
|
|
131
|
+
export function movieResponse(title: string = 'Spirited Away'): Response {
|
|
132
132
|
return FakeResponse.success(`
|
|
133
133
|
@prefix schema: <https://schema.org/>.
|
|
134
134
|
@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.
|
package/src/testing/setup.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import 'fake-indexeddb/auto';
|
|
2
2
|
|
|
3
|
-
import { installVitestSolidMatchers } from '@noeldemartin/solid-utils/
|
|
4
|
-
import { FakeLocalStorage, FakeServer, setupFacadeMocks } from '@noeldemartin/testing';
|
|
3
|
+
import { installVitestSolidMatchers } from '@noeldemartin/solid-utils/vitest';
|
|
4
|
+
import { FakeLocalStorage, FakeServer, mock, setupFacadeMocks } from '@noeldemartin/testing';
|
|
5
5
|
import { resetAsyncMemo } from '@noeldemartin/utils';
|
|
6
6
|
import { beforeEach, vi } from 'vitest';
|
|
7
7
|
|
|
8
8
|
setupFacadeMocks();
|
|
9
9
|
installVitestSolidMatchers();
|
|
10
|
+
globalThis.location = mock<Location>({ href: 'https://example.com' });
|
|
10
11
|
|
|
11
12
|
beforeEach(async () => {
|
|
12
13
|
const { Solid } = await import('@aerogel/plugin-solid');
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { SolidHasManyRelation } from 'soukai-solid';
|
|
2
|
+
import type { Relation } from 'soukai';
|
|
3
|
+
|
|
4
|
+
import Model from './Person.schema';
|
|
5
|
+
import Post from './Post';
|
|
6
|
+
|
|
7
|
+
export default class Person extends Model {
|
|
8
|
+
|
|
9
|
+
public static override timestamps = false;
|
|
10
|
+
|
|
11
|
+
declare public posts?: Post[];
|
|
12
|
+
declare public relatedPosts: SolidHasManyRelation<Person, Post, typeof Post>;
|
|
13
|
+
|
|
14
|
+
public postsRelationship(): Relation {
|
|
15
|
+
return this.hasMany(Post, 'authorUrl');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { FieldType } from 'soukai';
|
|
2
|
+
import { defineSolidModelSchema } from 'soukai-solid';
|
|
3
|
+
|
|
4
|
+
export default defineSolidModelSchema({
|
|
5
|
+
rdfContext: 'https://schema.org/',
|
|
6
|
+
rdfsClass: 'Article',
|
|
7
|
+
fields: {
|
|
8
|
+
title: {
|
|
9
|
+
type: FieldType.String,
|
|
10
|
+
rdfProperty: 'name',
|
|
11
|
+
},
|
|
12
|
+
authorUrl: {
|
|
13
|
+
type: FieldType.Key,
|
|
14
|
+
rdfProperty: 'author',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Relation } from 'soukai';
|
|
2
|
+
import type { SolidBelongsToOneRelation } from 'soukai-solid';
|
|
3
|
+
|
|
4
|
+
import Model from './Post.schema';
|
|
5
|
+
import Person from './Person';
|
|
6
|
+
|
|
7
|
+
export default class Post extends Model {
|
|
8
|
+
|
|
9
|
+
public static override timestamps = false;
|
|
10
|
+
|
|
11
|
+
declare public author?: Person;
|
|
12
|
+
declare public relatedAuthor: SolidBelongsToOneRelation<Post, Person, typeof Person>;
|
|
13
|
+
|
|
14
|
+
public authorRelationship(): Relation {
|
|
15
|
+
return this.belongsToOne(Person, 'authorUrl');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { events as n, fetch as t, handleIncomingRedirect as c, login as g, logout as i } from "@inrupt/solid-client-authn-browser";
|
|
2
|
-
export {
|
|
3
|
-
n as events,
|
|
4
|
-
t as fetch,
|
|
5
|
-
c as handleIncomingRedirect,
|
|
6
|
-
g as login,
|
|
7
|
-
i as logout
|
|
8
|
-
};
|
|
9
|
-
//# sourceMappingURL=InruptAuthenticator.lazy-BvpQLGoj-BvpQLGoj.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"InruptAuthenticator.lazy-BvpQLGoj-BvpQLGoj.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"LegacyAuthenticator.lazy-ChFFqP14-ChFFqP14.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<AGSolidStatus>
|
|
3
|
-
<template #logged-in="{ session }: IAGSolidStatusLoggedInSlotProps">
|
|
4
|
-
<AGMarkdown v-if="$cloud.syncing" :text="$td('cloud.syncing', 'Syncing...')" />
|
|
5
|
-
<div v-else class="flex flex-col gap-3">
|
|
6
|
-
<AGMarkdown
|
|
7
|
-
:text="
|
|
8
|
-
$td('solid.loggedIn', 'You are logged in as [{userName}]({userWebId})', {
|
|
9
|
-
userName: session.user.name ?? session.user.webId,
|
|
10
|
-
userWebId: session.user.webId,
|
|
11
|
-
})
|
|
12
|
-
"
|
|
13
|
-
/>
|
|
14
|
-
<AGButton @click="$cloud.sync(), $emit('sync')">
|
|
15
|
-
{{ $td('cloud.sync', 'Synchronize') }}
|
|
16
|
-
</AGButton>
|
|
17
|
-
<AGButton @click="$solid.logout()">
|
|
18
|
-
{{ $td('solid.logout', 'Logout') }}
|
|
19
|
-
</AGButton>
|
|
20
|
-
</div>
|
|
21
|
-
</template>
|
|
22
|
-
</AGSolidStatus>
|
|
23
|
-
</template>
|
|
24
|
-
|
|
25
|
-
<script setup lang="ts">
|
|
26
|
-
import { AGButton, AGMarkdown } from '@aerogel/core';
|
|
27
|
-
import { AGSolidStatus } from '@aerogel/plugin-solid';
|
|
28
|
-
import type { IAGSolidStatusLoggedInSlotProps } from '@aerogel/plugin-solid';
|
|
29
|
-
|
|
30
|
-
defineEmits(['sync']);
|
|
31
|
-
</script>
|