@comapeo/core 4.3.0 → 5.0.0-next.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/blob-store/downloader.d.ts +5 -2
  2. package/dist/blob-store/downloader.d.ts.map +1 -1
  3. package/dist/constants.d.ts +0 -1
  4. package/dist/constants.d.ts.map +1 -1
  5. package/dist/core-ownership.d.ts +2 -6
  6. package/dist/core-ownership.d.ts.map +1 -1
  7. package/dist/datatype/index.d.ts +30 -30
  8. package/dist/datatype/index.d.ts.map +1 -1
  9. package/dist/discovery/local-discovery.d.ts.map +1 -1
  10. package/dist/import-categories.d.ts +19 -0
  11. package/dist/import-categories.d.ts.map +1 -0
  12. package/dist/intl/iso639.d.ts +4 -0
  13. package/dist/intl/iso639.d.ts.map +1 -0
  14. package/dist/intl/parse-bcp-47.d.ts +22 -0
  15. package/dist/intl/parse-bcp-47.d.ts.map +1 -0
  16. package/dist/invite/invite-api.d.ts.map +1 -1
  17. package/dist/lib/drizzle-helpers.d.ts +19 -1
  18. package/dist/lib/drizzle-helpers.d.ts.map +1 -1
  19. package/dist/mapeo-manager.d.ts +15 -9
  20. package/dist/mapeo-manager.d.ts.map +1 -1
  21. package/dist/mapeo-project.d.ts +4969 -3017
  22. package/dist/mapeo-project.d.ts.map +1 -1
  23. package/dist/schema/client.d.ts +246 -232
  24. package/dist/schema/client.d.ts.map +1 -1
  25. package/dist/schema/comapeo-to-drizzle.d.ts +65 -0
  26. package/dist/schema/comapeo-to-drizzle.d.ts.map +1 -0
  27. package/dist/schema/json-schema-to-drizzle.d.ts +18 -0
  28. package/dist/schema/json-schema-to-drizzle.d.ts.map +1 -0
  29. package/dist/schema/project.d.ts +2711 -1835
  30. package/dist/schema/project.d.ts.map +1 -1
  31. package/dist/schema/types.d.ts +73 -66
  32. package/dist/schema/types.d.ts.map +1 -1
  33. package/dist/translation-api.d.ts +112 -192
  34. package/dist/translation-api.d.ts.map +1 -1
  35. package/dist/types.d.ts +9 -9
  36. package/dist/types.d.ts.map +1 -1
  37. package/dist/utils.d.ts +25 -3
  38. package/dist/utils.d.ts.map +1 -1
  39. package/drizzle/client/0004_glorious_shape.sql +1 -0
  40. package/drizzle/client/meta/0000_snapshot.json +13 -9
  41. package/drizzle/client/meta/0001_snapshot.json +13 -9
  42. package/drizzle/client/meta/0002_snapshot.json +13 -9
  43. package/drizzle/client/meta/0003_snapshot.json +13 -9
  44. package/drizzle/client/meta/0004_snapshot.json +239 -0
  45. package/drizzle/client/meta/_journal.json +7 -0
  46. package/drizzle/project/meta/0000_snapshot.json +43 -24
  47. package/drizzle/project/meta/0001_snapshot.json +47 -26
  48. package/drizzle/project/meta/0002_snapshot.json +47 -26
  49. package/package.json +17 -9
  50. package/src/constants.js +0 -3
  51. package/src/datatype/index.js +92 -39
  52. package/src/discovery/local-discovery.js +3 -2
  53. package/src/import-categories.js +368 -0
  54. package/src/index-writer/index.js +1 -1
  55. package/src/intl/iso639.js +8118 -0
  56. package/src/intl/parse-bcp-47.js +91 -0
  57. package/src/invite/invite-api.js +2 -0
  58. package/src/lib/drizzle-helpers.js +70 -18
  59. package/src/mapeo-manager.js +138 -88
  60. package/src/mapeo-project.js +72 -229
  61. package/src/roles.js +1 -1
  62. package/src/schema/client.js +22 -28
  63. package/src/schema/comapeo-to-drizzle.js +57 -0
  64. package/src/schema/{schema-to-drizzle.js → json-schema-to-drizzle.js} +25 -25
  65. package/src/schema/project.js +24 -37
  66. package/src/schema/types.ts +138 -99
  67. package/src/translation-api.js +65 -13
  68. package/src/types.ts +11 -20
  69. package/src/utils.js +37 -3
  70. package/dist/config-import.d.ts +0 -74
  71. package/dist/config-import.d.ts.map +0 -1
  72. package/dist/schema/schema-to-drizzle.d.ts +0 -20
  73. package/dist/schema/schema-to-drizzle.d.ts.map +0 -1
  74. package/dist/schema/utils.d.ts +0 -55
  75. package/dist/schema/utils.d.ts.map +0 -1
  76. package/src/config-import.js +0 -603
  77. package/src/schema/utils.js +0 -51
@@ -2,7 +2,7 @@ import path from 'path'
2
2
  import Database from 'better-sqlite3'
3
3
  import { decodeBlockPrefix, decode, parseVersionId } from '@comapeo/schema'
4
4
  import { drizzle } from 'drizzle-orm/better-sqlite3'
5
- import { sql, count } from 'drizzle-orm'
5
+ import { sql, count, eq } from 'drizzle-orm'
6
6
  import { discoveryKey } from 'hypercore-crypto'
7
7
  import { TypedEmitter } from 'tiny-typed-emitter'
8
8
  import ZipArchive from 'zip-stream-promise'
@@ -11,7 +11,7 @@ import mime from 'mime/lite'
11
11
  // @ts-expect-error
12
12
  import { Readable, pipelinePromise } from 'streamx'
13
13
 
14
- import { NAMESPACES, NAMESPACE_SCHEMAS, UNIX_EPOCH_DATE } from './constants.js'
14
+ import { NAMESPACES, NAMESPACE_SCHEMAS } from './constants.js'
15
15
  import { CoreManager } from './core-manager/index.js'
16
16
  import { DataStore } from './datastore/index.js'
17
17
  import { DataType, kCreateWithDocId } from './datatype/index.js'
@@ -61,11 +61,12 @@ import {
61
61
  } from './sync/sync-api.js'
62
62
  import { Logger } from './logger.js'
63
63
  import { IconApi } from './icon-api.js'
64
- import { readConfig } from './config-import.js'
64
+ import { importCategories } from './import-categories.js'
65
65
  import TranslationApi from './translation-api.js'
66
66
  import { NotFoundError, nullIfNotFound } from './errors.js'
67
67
  import { WebSocket } from 'ws'
68
68
  import { createWriteStream } from 'fs'
69
+ import ensureError from 'ensure-error'
69
70
  /** @import { ProjectSettingsValue, Observation, Track } from '@comapeo/schema' */
70
71
  /** @import { Attachment, CoreStorage, BlobFilter, BlobId, BlobStoreEntriesStream, KeyPair, Namespace, ReplicationStream, GenericBlobFilter, MapeoValueMap, MapeoDocMap } from './types.js' */
71
72
  /** @typedef {Omit<ProjectSettingsValue, 'schemaName'>} EditableProjectSettings */
@@ -97,12 +98,12 @@ export const kBlobStore = Symbol('blobStore')
97
98
  export const kProjectReplicate = Symbol('replicate project')
98
99
  export const kDataTypes = Symbol('dataTypes')
99
100
  export const kProjectLeave = Symbol('leave project')
100
- export const kClearDataIfLeft = Symbol('clear data if left project')
101
+ export const kClearData = Symbol('clear project data')
101
102
  export const kSetIsArchiveDevice = Symbol('set isArchiveDevice')
102
103
  export const kIsArchiveDevice = Symbol('isArchiveDevice (temp - test only)')
103
104
  export const kGeoJSONFileName = Symbol('geoJSONFileName')
104
105
 
105
- const EMPTY_PROJECT_SETTINGS = Object.freeze({})
106
+ const EMPTY_PROJECT_SETTINGS = Object.freeze({ sendStats: false })
106
107
 
107
108
  /** @type BlobId['variant'][]*/
108
109
  const VARIANT_EXPORT_ORDER = ['original', 'preview', 'thumbnail']
@@ -130,9 +131,10 @@ export class MapeoProject extends TypedEmitter {
130
131
  #translationApi
131
132
  #l
132
133
  /** @type {Boolean} this avoids loading multiple configs in parallel */
133
- #loadingConfig
134
+ #importingCategories
134
135
 
135
136
  static EMPTY_PROJECT_SETTINGS = EMPTY_PROJECT_SETTINGS
137
+ #getFallbackProjectInfo
136
138
 
137
139
  /**
138
140
  * @param {Object} opts
@@ -149,6 +151,7 @@ export class MapeoProject extends TypedEmitter {
149
151
  * @param {(url: string) => WebSocket} [opts.makeWebsocket]
150
152
  * @param {import('./local-peers.js').LocalPeers} opts.localPeers
151
153
  * @param {boolean} opts.isArchiveDevice Whether this device is an archive device
154
+ * @param {() => import('./schema/client.js').ProjectInfo | undefined} opts.getFallbackProjectInfo
152
155
  * @param {Logger} [opts.logger]
153
156
  *
154
157
  */
@@ -167,13 +170,15 @@ export class MapeoProject extends TypedEmitter {
167
170
  localPeers,
168
171
  logger,
169
172
  isArchiveDevice,
173
+ getFallbackProjectInfo,
170
174
  }) {
171
175
  super()
172
176
 
173
177
  this.#l = Logger.create('project', logger)
174
178
  this.#deviceId = getDeviceId(keyManager)
175
179
  this.#projectKey = projectKey
176
- this.#loadingConfig = false
180
+ this.#importingCategories = false
181
+ this.#getFallbackProjectInfo = getFallbackProjectInfo
177
182
 
178
183
  const getReplicationStream = this[kProjectReplicate].bind(this, true)
179
184
 
@@ -216,6 +221,11 @@ export class MapeoProject extends TypedEmitter {
216
221
 
217
222
  if (reindex) {
218
223
  for (const table of indexedTables) db.delete(table).run()
224
+
225
+ sharedDb
226
+ .delete(projectSettingsTable)
227
+ .where(eq(projectSettingsTable.docId, this.#projectId))
228
+ .run()
219
229
  }
220
230
 
221
231
  ///////// 3. Setup random-access-storage functions
@@ -287,74 +297,78 @@ export class MapeoProject extends TypedEmitter {
287
297
 
288
298
  /** @type {typeof TranslationApi.prototype.get} */
289
299
  const getTranslations = (...args) => this.$translation.get(...args)
300
+ /** @type {(versionId: string) => Promise<string>} */
301
+ const getDeviceIdForVersionId = (...args) =>
302
+ this.$originalVersionIdToDeviceId(...args)
303
+
290
304
  this.#dataTypes = {
291
305
  observation: new DataType({
292
306
  dataStore: this.#dataStores.data,
293
307
  table: observationTable,
294
308
  db,
295
- getTranslations,
309
+ getDeviceIdForVersionId,
296
310
  }),
297
311
  track: new DataType({
298
312
  dataStore: this.#dataStores.data,
299
313
  table: trackTable,
300
314
  db,
301
- getTranslations,
315
+ getDeviceIdForVersionId,
302
316
  }),
303
317
  remoteDetectionAlert: new DataType({
304
318
  dataStore: this.#dataStores.data,
305
319
  table: remoteDetectionAlertTable,
306
320
  db,
307
- getTranslations,
321
+ getDeviceIdForVersionId,
308
322
  }),
309
323
  preset: new DataType({
310
324
  dataStore: this.#dataStores.config,
311
325
  table: presetTable,
312
326
  db,
313
327
  getTranslations,
328
+ getDeviceIdForVersionId,
314
329
  }),
315
330
  field: new DataType({
316
331
  dataStore: this.#dataStores.config,
317
332
  table: fieldTable,
318
333
  db,
319
334
  getTranslations,
335
+ getDeviceIdForVersionId,
320
336
  }),
321
337
  projectSettings: new DataType({
322
338
  dataStore: this.#dataStores.config,
323
339
  table: projectSettingsTable,
324
340
  db: sharedDb,
325
- getTranslations,
341
+ getDeviceIdForVersionId,
326
342
  }),
327
343
  coreOwnership: new DataType({
328
344
  dataStore: this.#dataStores.auth,
329
345
  table: coreOwnershipTable,
330
346
  db,
331
- getTranslations,
347
+ getDeviceIdForVersionId: () => Promise.resolve(''),
332
348
  }),
333
349
  role: new DataType({
334
350
  dataStore: this.#dataStores.auth,
335
351
  table: roleTable,
336
352
  db,
337
- getTranslations,
353
+ getDeviceIdForVersionId: () => Promise.resolve(''),
338
354
  }),
339
355
  deviceInfo: new DataType({
340
356
  dataStore: this.#dataStores.config,
341
357
  table: deviceInfoTable,
342
358
  db,
343
- getTranslations,
359
+ getDeviceIdForVersionId: () => Promise.resolve(''),
344
360
  }),
345
361
  icon: new DataType({
346
362
  dataStore: this.#dataStores.config,
347
363
  table: iconTable,
348
364
  db,
349
- getTranslations,
365
+ getDeviceIdForVersionId,
350
366
  }),
351
367
  translation: new DataType({
352
368
  dataStore: this.#dataStores.config,
353
369
  table: translationTable,
354
370
  db,
355
- getTranslations: () => {
356
- throw new Error('Cannot get translation for translations')
357
- },
371
+ getDeviceIdForVersionId,
358
372
  }),
359
373
  }
360
374
  this.#identityKeypair = keyManager.getIdentityKeypair()
@@ -675,7 +689,12 @@ export class MapeoProject extends TypedEmitter {
675
689
  await this.#dataTypes.projectSettings.getByDocId(this.#projectId)
676
690
  )
677
691
  } catch (e) {
678
- return /** @type {EditableProjectSettings} */ (EMPTY_PROJECT_SETTINGS)
692
+ // if (e instanceof Error && e.name !== 'NotFoundError') throw e
693
+ // If the project has not completed an initial sync, project settings will
694
+ // not be available, so use fallback project info which is set from the
695
+ // invite that was used to join the project.
696
+ const fallbackInfo = this.#getFallbackProjectInfo()
697
+ return fallbackInfo || EMPTY_PROJECT_SETTINGS
679
698
  }
680
699
  }
681
700
 
@@ -684,11 +703,9 @@ export class MapeoProject extends TypedEmitter {
684
703
  */
685
704
  async $hasSyncedProjectSettings() {
686
705
  try {
687
- const settings = await this.#dataTypes.projectSettings.getByDocId(
688
- this.#projectId
689
- )
690
-
691
- return settings.createdAt !== UNIX_EPOCH_DATE
706
+ // Should error if we haven't synced before
707
+ await this.#dataTypes.projectSettings.getByDocId(this.#projectId)
708
+ return true
692
709
  } catch (e) {
693
710
  return false
694
711
  }
@@ -706,6 +723,7 @@ export class MapeoProject extends TypedEmitter {
706
723
  }
707
724
 
708
725
  /**
726
+ * @deprecated
709
727
  * @param {string} originalVersionId The `originalVersionId` from a document.
710
728
  * @returns {Promise<string>} The device ID for this creator.
711
729
  * @throws When device ID cannot be found.
@@ -1297,19 +1315,14 @@ export class MapeoProject extends TypedEmitter {
1297
1315
 
1298
1316
  await this.#roles.assignRole(this.#deviceId, LEFT_ROLE_ID)
1299
1317
 
1300
- await this[kClearDataIfLeft]()
1318
+ await this[kClearData]()
1301
1319
  }
1302
1320
 
1303
1321
  /**
1304
- * Clear data if we've left the project. No-op if you're still in the project.
1322
+ * Clear synced data, but keep auth data and own data
1305
1323
  * @returns {Promise<void>}
1306
1324
  */
1307
- async [kClearDataIfLeft]() {
1308
- const role = await this.$getOwnRole()
1309
- if (role.roleId !== LEFT_ROLE_ID) {
1310
- return
1311
- }
1312
-
1325
+ async [kClearData]() {
1313
1326
  const namespacesWithoutAuth =
1314
1327
  /** @satisfies {Exclude<Namespace, 'auth'>[]} */ ([
1315
1328
  'config',
@@ -1340,172 +1353,44 @@ export class MapeoProject extends TypedEmitter {
1340
1353
  }
1341
1354
  }
1342
1355
 
1343
- /** @param {Object} opts
1344
- * @param {string} opts.configPath
1345
- * @returns {Promise<Error[]>}
1356
+ /**
1357
+ * @deprecated
1358
+ * @param {object} opts
1359
+ * @param {string} opts.configPath
1360
+ * @returns {Promise<Error[]>}
1346
1361
  */
1347
1362
  async importConfig({ configPath }) {
1363
+ try {
1364
+ await this.$importCategories({ filePath: configPath })
1365
+ return []
1366
+ } catch (e) {
1367
+ return [ensureError(e)]
1368
+ }
1369
+ }
1370
+
1371
+ /**
1372
+ * @param {object} opts
1373
+ * @param {string} opts.filePath
1374
+ * @returns {Promise<void>}
1375
+ */
1376
+ async $importCategories({ filePath }) {
1348
1377
  assert(
1349
- !this.#loadingConfig,
1350
- 'Cannot run multiple config imports at the same time'
1378
+ !this.#importingCategories,
1379
+ 'Cannot run multiple category imports at the same time'
1351
1380
  )
1352
- this.#loadingConfig = true
1381
+ this.#importingCategories = true
1353
1382
 
1354
1383
  try {
1355
- // check for already present fields and presets and delete them if exist
1356
- const presetsToDelete = await grabDocsToDelete(this.preset)
1357
- const fieldsToDelete = await grabDocsToDelete(this.field)
1358
- // delete only translations that refer to deleted fields and presets
1359
- const translationsToDelete = await grabTranslationsToDelete({
1360
- logger: this.#l,
1361
- translation: this.$translation.dataType,
1362
- preset: this.preset,
1363
- field: this.field,
1364
- })
1365
-
1366
- const config = await readConfig(configPath)
1367
- /** @type {Map<string, import('./icon-api.js').IconRef>} */
1368
- const iconNameToRef = new Map()
1369
- /** @type {Map<string, import('@comapeo/schema').PresetValue['fieldRefs'][1]>} */
1370
- const fieldNameToRef = new Map()
1371
- /** @type {Map<string,import('@comapeo/schema').TranslationValue['docRef']>} */
1372
- const presetNameToRef = new Map()
1373
-
1374
- // Do this in serial not parallel to avoid memory issues (avoid keeping all icon buffers in memory)
1375
- for await (const icon of config.icons()) {
1376
- const { docId, versionId } = await this.#iconApi.create(icon)
1377
- iconNameToRef.set(icon.name, { docId, versionId })
1378
- }
1379
-
1380
- // Ok to create fields and presets in parallel
1381
- const fieldPromises = []
1382
- for (const { name, value } of config.fields()) {
1383
- fieldPromises.push(
1384
- this.#dataTypes.field.create(value).then(({ docId, versionId }) => {
1385
- fieldNameToRef.set(name, { docId, versionId })
1386
- })
1387
- )
1388
- }
1389
- await Promise.all(fieldPromises)
1390
-
1391
- const presetsWithRefs = []
1392
- for (const { fieldNames, iconName, value, name } of config.presets()) {
1393
- const fieldRefs = fieldNames.map((fieldName) => {
1394
- const fieldRef = fieldNameToRef.get(fieldName)
1395
- if (!fieldRef) {
1396
- throw new NotFoundError(
1397
- `field ${fieldName} not found (referenced by preset ${value.name})})`
1398
- )
1399
- }
1400
- return fieldRef
1401
- })
1402
-
1403
- if (!iconName) {
1404
- throw new Error(`preset ${value.name} is missing an icon name`)
1405
- }
1406
- const iconRef = iconNameToRef.get(iconName)
1407
- if (!iconRef) {
1408
- throw new NotFoundError(
1409
- `icon ${iconName} not found (referenced by preset ${value.name})`
1410
- )
1411
- }
1412
-
1413
- presetsWithRefs.push({
1414
- preset: {
1415
- ...value,
1416
- iconRef,
1417
- fieldRefs,
1418
- },
1419
- name,
1420
- })
1421
- }
1422
-
1423
- const presetPromises = []
1424
- for (const { preset, name } of presetsWithRefs) {
1425
- presetPromises.push(
1426
- this.preset.create(preset).then(({ docId, versionId }) => {
1427
- presetNameToRef.set(name, { docId, versionId })
1428
- })
1429
- )
1430
- }
1431
-
1432
- await Promise.all(presetPromises)
1433
-
1434
- const translationPromises = []
1435
- for (const { name, value } of config.translations()) {
1436
- let docRef
1437
- if (value.docRefType === 'field') {
1438
- docRef = { ...fieldNameToRef.get(name) }
1439
- } else if (value.docRefType === 'preset') {
1440
- docRef = { ...presetNameToRef.get(name) }
1441
- } else {
1442
- throw new Error(`invalid docRefType ${value.docRefType}`)
1443
- }
1444
- if (docRef.docId && docRef.versionId) {
1445
- translationPromises.push(
1446
- this.$translation.put({
1447
- ...value,
1448
- docRef: {
1449
- docId: docRef.docId,
1450
- versionId: docRef.versionId,
1451
- },
1452
- })
1453
- )
1454
- } else {
1455
- throw new NotFoundError(
1456
- `docRef for ${value.docRefType} with name ${name} not found`
1457
- )
1458
- }
1459
- }
1460
- await Promise.all(translationPromises)
1461
-
1462
- // close the zip handles after we know we won't be needing them anymore
1463
- await config.close()
1464
- const presetIds = [...presetNameToRef.values()].map((val) => val.docId)
1465
-
1466
- await this.$setProjectSettings({
1467
- defaultPresets: {
1468
- point: presetIds,
1469
- line: [],
1470
- area: [],
1471
- vertex: [],
1472
- relation: [],
1473
- },
1474
- configMetadata: config.metadata,
1475
- })
1476
-
1477
- const deletePresetsPromise = Promise.all(
1478
- presetsToDelete.map(async (docId) => {
1479
- const { deleted } = await this.preset.getByDocId(docId)
1480
- if (!deleted) await this.preset.delete(docId)
1481
- })
1482
- )
1483
- const deleteFieldsPromise = Promise.all(
1484
- fieldsToDelete.map(async (docId) => {
1485
- const { deleted } = await this.field.getByDocId(docId)
1486
- if (!deleted) await this.field.delete(docId)
1487
- })
1488
- )
1489
- const deleteTranslationsPromise = Promise.all(
1490
- [...translationsToDelete].map(async (docId) => {
1491
- const { deleted } = await this.$translation.dataType.getByDocId(docId)
1492
- if (!deleted) await this.$translation.dataType.delete(docId)
1493
- })
1494
- )
1495
- await Promise.all([
1496
- deletePresetsPromise,
1497
- deleteFieldsPromise,
1498
- deleteTranslationsPromise,
1499
- ])
1500
- this.#loadingConfig = false
1501
- return config.warnings
1384
+ await importCategories(this, { filePath, logger: this.#l })
1502
1385
  } catch (e) {
1503
1386
  this.#l.log('error loading config', e)
1504
- this.#loadingConfig = false
1505
- return /** @type Error[] */ []
1387
+ throw e
1388
+ } finally {
1389
+ this.#importingCategories = false
1506
1390
  }
1507
1391
  }
1508
1392
  }
1393
+
1509
1394
  /**
1510
1395
  * @param {import("@comapeo/schema").ProjectSettings & { forks: string[] }} projectDoc
1511
1396
  * @returns {EditableProjectSettings}
@@ -1514,48 +1399,6 @@ function extractEditableProjectSettings(projectDoc) {
1514
1399
  return omit(valueOf(projectDoc), ['schemaName'])
1515
1400
  }
1516
1401
 
1517
- /**
1518
- @param {MapeoProject['field'] | MapeoProject['preset']} dataType
1519
- @returns {Promise<String[]>}
1520
- */
1521
- async function grabDocsToDelete(dataType) {
1522
- const toDelete = []
1523
- for (const { docId } of await dataType.getMany()) {
1524
- toDelete.push(docId)
1525
- }
1526
- return toDelete
1527
- }
1528
-
1529
- /**
1530
- * @param {Object} opts
1531
- * @param {Logger} opts.logger
1532
- * @param {MapeoProject['$translation']['dataType']} opts.translation
1533
- * @param {MapeoProject['preset']} opts.preset
1534
- * @param {MapeoProject['field']} opts.field
1535
- * @returns {Promise<Set<String>>}
1536
- */
1537
- async function grabTranslationsToDelete(opts) {
1538
- /** @type {Set<String>} */
1539
- const toDelete = new Set()
1540
- const translations = await opts.translation.getMany()
1541
- await Promise.all(
1542
- translations.map(async ({ docRefType, docRef, docId }) => {
1543
- if (docRefType === 'field' || docRefType === 'preset') {
1544
- let doc
1545
- try {
1546
- doc = await opts[docRefType].getByVersionId(docRef.versionId)
1547
- } catch (e) {
1548
- opts.logger.log(`referred ${docRef.versionId} is not found`)
1549
- }
1550
- if (doc) {
1551
- toDelete.add(docId)
1552
- }
1553
- }
1554
- })
1555
- )
1556
- return toDelete
1557
- }
1558
-
1559
1402
  /**
1560
1403
  * Return a map of namespace -> core keypair
1561
1404
  *
package/src/roles.js CHANGED
@@ -148,7 +148,7 @@ export const NO_ROLE = {
148
148
  roleAssignment: [],
149
149
  sync: {
150
150
  auth: 'allowed',
151
- config: 'allowed',
151
+ config: 'blocked',
152
152
  data: 'blocked',
153
153
  blobIndex: 'blocked',
154
154
  blob: 'blocked',
@@ -3,55 +3,49 @@
3
3
  // device
4
4
  import { blob, sqliteTable, text, int } from 'drizzle-orm/sqlite-core'
5
5
  import { dereferencedDocSchemas as schemas } from '@comapeo/schema'
6
- import { jsonSchemaToDrizzleColumns as toColumns } from './schema-to-drizzle.js'
7
- import { backlinkTable, customJson } from './utils.js'
6
+ import {
7
+ comapeoSchemaToDrizzleTable as toDrizzle,
8
+ backlinkTable,
9
+ } from './comapeo-to-drizzle.js'
8
10
 
9
11
  /**
10
12
  * @import { ProjectSettings } from '@comapeo/schema'
13
+ * @import { $Type } from 'drizzle-orm'
14
+ * @import { SQLiteTextJsonBuilder } from 'drizzle-orm/sqlite-core'
11
15
  *
12
16
  * @internal
13
- * @typedef {Pick<ProjectSettings, 'name' | 'projectColor' | 'projectDescription'>} ProjectInfo
17
+ * @typedef {Pick<ProjectSettings, 'name' | 'projectColor' | 'projectDescription' | 'sendStats'>} ProjectInfo
14
18
  */
15
19
 
16
- const projectInfoColumn =
17
- /** @type {ReturnType<typeof import('drizzle-orm/sqlite-core').customType<{data: ProjectInfo}>>} */ (
18
- customJson
19
- )
20
-
21
20
  /** @type {ProjectInfo} */
22
- const PROJECT_INFO_DEFAULT_VALUE = {}
21
+ const PROJECT_INFO_DEFAULT_VALUE = { sendStats: false }
23
22
 
24
- export const projectSettingsTable = sqliteTable(
25
- 'projectSettings',
26
- toColumns(schemas.projectSettings)
27
- )
28
- export const projectBacklinkTable = backlinkTable(projectSettingsTable)
23
+ export const projectSettingsTable = toDrizzle(schemas.projectSettings)
24
+ export const projectBacklinkTable = backlinkTable('projectSettings')
29
25
  export const projectKeysTable = sqliteTable('projectKeys', {
30
26
  projectId: text('projectId').notNull().primaryKey(),
31
27
  projectPublicId: text('projectPublicId').notNull(),
32
- projectInviteId: blob('projectInviteId').notNull(),
28
+ projectInviteId: blob('projectInviteId', { mode: 'buffer' }).notNull(),
33
29
  keysCipher: blob('keysCipher', { mode: 'buffer' }).notNull(),
34
- projectInfo: projectInfoColumn('projectInfo')
35
- .default(
36
- // TODO: There's a bug in Drizzle where the default value does not get transformed by the custom type
37
- // @ts-expect-error
38
- JSON.stringify(PROJECT_INFO_DEFAULT_VALUE)
39
- )
40
- .notNull(),
30
+ projectInfo:
31
+ /** @type {$Type<SQLiteTextJsonBuilder, ProjectInfo>} */
32
+ (text('projectInfo', { mode: 'json' }))
33
+ .default(PROJECT_INFO_DEFAULT_VALUE)
34
+ .notNull(),
35
+ hasLeftProject: int('hasLeftProject', { mode: 'boolean' })
36
+ .notNull()
37
+ .default(false),
41
38
  })
42
39
 
43
40
  /**
44
41
  * @typedef {Omit<import('@comapeo/schema').DeviceInfoValue, 'schemaName'>} DeviceInfoParam
45
42
  */
46
43
 
47
- const deviceInfoColumn =
48
- /** @type {ReturnType<typeof import('drizzle-orm/sqlite-core').customType<{data: DeviceInfoParam }>>} */ (
49
- customJson
50
- )
51
-
52
44
  // This table only ever has one row in it.
53
45
  export const deviceSettingsTable = sqliteTable('deviceSettings', {
54
46
  deviceId: text('deviceId').notNull().unique(),
55
- deviceInfo: deviceInfoColumn('deviceInfo'),
47
+ deviceInfo:
48
+ /** @type {$Type<SQLiteTextJsonBuilder, DeviceInfoParam>} */
49
+ (text('deviceInfo', { mode: 'json' })),
56
50
  isArchiveDevice: int('isArchiveDevice', { mode: 'boolean' }),
57
51
  })
@@ -0,0 +1,57 @@
1
+ import { text, sqliteTable } from 'drizzle-orm/sqlite-core'
2
+ import { jsonSchemaToDrizzleSqliteTable } from './json-schema-to-drizzle.js'
3
+
4
+ export const BACKLINK_TABLE_POSTFIX = '_backlink'
5
+
6
+ /**
7
+ * @import { JsonSchemaToDrizzleSqliteTable } from './types.js'
8
+ * @import { SQLiteTextJsonBuilder } from 'drizzle-orm/sqlite-core'
9
+ * @import { $Type } from 'drizzle-orm'
10
+ * @import { MapeoDocMap } from '../types.js'
11
+ */
12
+
13
+ /**
14
+ * @typedef {typeof import('@comapeo/schema').dereferencedDocSchemas} ComapeoSchemaMap
15
+ * @typedef {{ forks: $Type<SQLiteTextJsonBuilder, string[]> }} AdditionalColumns
16
+ */
17
+
18
+ /**
19
+ * @template {ComapeoSchemaMap[keyof ComapeoSchemaMap]} TSchema
20
+ * @param {TSchema} schema
21
+ * @returns {JsonSchemaToDrizzleSqliteTable<
22
+ * MapeoDocMap[TSchema['properties']['schemaName']['const']],
23
+ * TSchema,
24
+ * TSchema['properties']['schemaName']['const'],
25
+ * AdditionalColumns,
26
+ * 'docId'
27
+ * >}
28
+ */
29
+ export function comapeoSchemaToDrizzleTable(schema) {
30
+ return jsonSchemaToDrizzleSqliteTable(
31
+ schema.properties.schemaName.const,
32
+ schema,
33
+ {
34
+ additionalColumns: { forks: text('forks', { mode: 'json' }).notNull() },
35
+ primaryKey: 'docId',
36
+ }
37
+ )
38
+ }
39
+
40
+ /**
41
+ * Table for storing backlinks, used for indexing. There needs to be one for
42
+ * each indexed document type, with a specific name `<datatype>_backlink`
43
+ *
44
+ * @param {import('@comapeo/schema').MapeoDoc['schemaName']} schemaName
45
+ */
46
+ export function backlinkTable(schemaName) {
47
+ return sqliteTable(getBacklinkTableName(schemaName), {
48
+ versionId: text('versionId').notNull().primaryKey(),
49
+ })
50
+ }
51
+
52
+ /**
53
+ * @param {string} tableName
54
+ */
55
+ export function getBacklinkTableName(tableName) {
56
+ return tableName + BACKLINK_TABLE_POSTFIX
57
+ }