Skip to content

Commit

Permalink
Integrate changes from #968
Browse files Browse the repository at this point in the history
  • Loading branch information
EvanHahn committed Nov 18, 2024
1 parent 8df5ffc commit 12b21a2
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 111 deletions.
22 changes: 22 additions & 0 deletions src/datatype/get-if-exists.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NotFoundError } from '../errors.js'
/** @import { DataStore } from '../datastore/index.js' */
/** @import { MapeoDocMap, MapeoValueMap } from '../types.js' */
/** @import { DataType, MapeoDocTables } from './index.js' */

/**
* @template {MapeoDocTables} TTable
* @template {TTable['_']['name']} TSchemaName
* @template {MapeoDocMap[TSchemaName]} TDoc
* @template {MapeoValueMap[TSchemaName]} TValue
* @param {DataType<DataStore, TTable, TSchemaName, TDoc, TValue>} dataType
* @param {string} docId
* @returns {Promise<null | TDoc & { forks: string[] }>}
*/
export async function getByDocIdIfExists(dataType, docId) {
try {
return await dataType.getByDocId(docId)
} catch (err) {
if (err instanceof NotFoundError) return null
throw err
}
}
2 changes: 1 addition & 1 deletion src/datatype/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import TranslationApi from '../translation-api.js'
type MapeoDocTableName = `${MapeoDoc['schemaName']}Table`
type GetMapeoDocTables<T> = T[keyof T & MapeoDocTableName]
/** Union of Drizzle schema tables that correspond to MapeoDoc types (e.g. excluding backlink tables and other utility tables) */
type MapeoDocTables =
export type MapeoDocTables =
| GetMapeoDocTables<typeof import('../schema/project.js')>
| GetMapeoDocTables<typeof import('../schema/client.js')>
type MapeoDocTablesMap = {
Expand Down
9 changes: 3 additions & 6 deletions src/mapeo-project.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import { IconApi } from './icon-api.js'
import { readConfig } from './config-import.js'
import TranslationApi from './translation-api.js'
import { NotFoundError } from './errors.js'
import { getByDocIdIfExists } from './datatype/get-if-exists.js'
/** @import { ProjectSettingsValue } from '@comapeo/schema' */
/** @import { CoreStorage, KeyPair, Namespace, ReplicationStream } from './types.js' */

Expand Down Expand Up @@ -600,9 +601,7 @@ export class MapeoProject extends TypedEmitter {
async $setProjectSettings(settings) {
const { projectSettings } = this.#dataTypes

const existing = await projectSettings.getByDocId(this.#projectId, {
mustBeFound: false,
})
const existing = await getByDocIdIfExists(projectSettings, this.#projectId)

if (existing) {
return extractEditableProjectSettings(
Expand Down Expand Up @@ -710,9 +709,7 @@ export class MapeoProject extends TypedEmitter {
schemaName: /** @type {const} */ ('deviceInfo'),
}

const existingDoc = await deviceInfo.getByDocId(configCoreId, {
mustBeFound: false,
})
const existingDoc = await getByDocIdIfExists(deviceInfo, configCoreId)
if (existingDoc) {
return await deviceInfo.update(existingDoc.versionId, doc)
} else {
Expand Down
9 changes: 3 additions & 6 deletions src/roles.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import mapObject from 'map-obj'
import { kCreateWithDocId, kDataStore } from './datatype/index.js'
import { assert, setHas } from './utils.js'
import { NotFoundError } from './errors.js'
import { getByDocIdIfExists } from './datatype/get-if-exists.js'
import { TypedEmitter } from 'tiny-typed-emitter'
/** @import { Role as RoleRecord } from '@comapeo/schema' */
/** @import { ReadonlyDeep } from 'type-fest' */
Expand Down Expand Up @@ -297,9 +298,7 @@ export class Roles extends TypedEmitter {
* @returns {Promise<null | typeof CREATOR_ROLE_RECORD | RoleRecord>}
*/
async #getRoleRecord(deviceId) {
const result = await this.#dataType.getByDocId(deviceId, {
mustBeFound: false,
})
const result = await getByDocIdIfExists(this.#dataType, deviceId)
if (result) return result

// The project creator will have the creator role
Expand Down Expand Up @@ -535,9 +534,7 @@ export class Roles extends TypedEmitter {
}
}

const existingRoleDoc = await this.#dataType.getByDocId(deviceId, {
mustBeFound: false,
})
const existingRoleDoc = await getByDocIdIfExists(this.#dataType, deviceId)

if (existingRoleDoc) {
await this.#dataType.update(
Expand Down
3 changes: 2 additions & 1 deletion src/translation-api.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { and, sql } from 'drizzle-orm'
import { kCreateWithDocId, kSelect } from './datatype/index.js'
import { getByDocIdIfExists } from './datatype/get-if-exists.js'
import { hashObject } from './utils.js'
import { omit } from './lib/omit.js'
/** @import { Translation, TranslationValue } from '@comapeo/schema' */
Expand Down Expand Up @@ -49,7 +50,7 @@ export default class TranslationApi {
async put(value) {
const identifiers = omit(value, ['message'])
const docId = hashObject(identifiers)
const doc = await this.#dataType.getByDocId(docId, { mustBeFound: false })
const doc = await getByDocIdIfExists(this.#dataType, docId)
if (doc) {
return await this.#dataType.update(doc.versionId, value)
} else {
Expand Down
20 changes: 20 additions & 0 deletions test/data-type/get-if-exists.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { testenv } from './test-helpers.js'
import test, { describe } from 'node:test'
import assert from 'node:assert/strict'
import { getByDocIdIfExists } from '../../src/datatype/get-if-exists.js'
import { valueOf } from '@comapeo/schema'
import { generate } from '@mapeo/mock-data'

describe('getByDocIdIfExists', () => {
test('resolves with null if no document exists with that ID', async () => {
const { dataType } = await testenv()
assert.equal(await getByDocIdIfExists(dataType, 'foo bar'), null)
})

test('resolves with the document if it exists', async () => {
const { dataType } = await testenv()
const fixture = valueOf(generate('observation')[0])
const observation = await dataType.create(fixture)
assert(await getByDocIdIfExists(dataType, observation.docId))
})
})
110 changes: 13 additions & 97 deletions test/data-type.js → test/data-type/index.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { DataStore } from '../src/datastore/index.js'
import { DataStore } from '../../src/datastore/index.js'
import {
createCoreManager,
waitForCores,
replicate,
} from './helpers/core-manager.js'
} from '../helpers/core-manager.js'
import RAM from 'random-access-memory'
import crypto from 'hypercore-crypto'
import { observationTable, translationTable } from '../src/schema/project.js'
import { DataType, kCreateWithDocId } from '../src/datatype/index.js'
import { IndexWriter } from '../src/index-writer/index.js'
import { NotFoundError } from '../src/errors.js'
import { observationTable } from '../../src/schema/project.js'
import { DataType, kCreateWithDocId } from '../../src/datatype/index.js'
import { IndexWriter } from '../../src/index-writer/index.js'
import { NotFoundError } from '../../src/errors.js'

import Database from 'better-sqlite3'
import { drizzle } from 'drizzle-orm/better-sqlite3'
import { migrate } from 'drizzle-orm/better-sqlite3/migrator'
import { randomBytes } from 'crypto'
import TranslationApi from '../src/translation-api.js'
import { getProperty } from 'dot-prop'
import { decode, decodeBlockPrefix, parseVersionId } from '@comapeo/schema'
import { parseVersionId } from '@comapeo/schema'

import { testenv } from './test-helpers.js'

/** @type {import('@comapeo/schema').ObservationValue} */
const obsFixture = {
Expand All @@ -45,7 +46,8 @@ test('private createWithDocId() method', async () => {
const sqlite = new Database(':memory:')
const db = drizzle(sqlite)
migrate(db, {
migrationsFolder: new URL('../drizzle/project', import.meta.url).pathname,
migrationsFolder: new URL('../../drizzle/project', import.meta.url)
.pathname,
})

const coreManager = createCoreManager()
Expand Down Expand Up @@ -81,7 +83,8 @@ test('private createWithDocId() method throws when doc exists', async () => {
const sqlite = new Database(':memory:')
const db = drizzle(sqlite)
migrate(db, {
migrationsFolder: new URL('../drizzle/project', import.meta.url).pathname,
migrationsFolder: new URL('../../drizzle/project', import.meta.url)
.pathname,
})

const coreManager = createCoreManager()
Expand Down Expand Up @@ -209,14 +212,6 @@ test('getByDocId() throws if no document exists with that ID', async () => {
await assert.rejects(() => dataType.getByDocId('foo bar'), NotFoundError)
})

test('getByDocId() can return null if no document exists with that ID', async () => {
const { dataType } = await testenv({ projectKey: randomBytes(32) })
assert.equal(
await dataType.getByDocId('foo bar', { mustBeFound: false }),
null
)
})

test('delete()', async () => {
const projectKey = randomBytes(32)
const { dataType } = await testenv({ projectKey })
Expand Down Expand Up @@ -311,82 +306,3 @@ test('translation', async () => {
`not passing a a language code returns the untranslated message`
)
})

/**
* @param {object} [opts={}]
* @param {Buffer} [opts.projectKey]
*/
async function testenv(opts = {}) {
const sqlite = new Database(':memory:')
const db = drizzle(sqlite)
migrate(db, {
migrationsFolder: new URL('../drizzle/project', import.meta.url).pathname,
})

const coreManager = createCoreManager({ ...opts, db })

const indexWriter = new IndexWriter({
tables: [observationTable, translationTable],
sqlite,
})

const dataStore = new DataStore({
coreManager,
namespace: 'data',
batch: async (entries) => indexWriter.batch(entries),
storage: () => new RAM(),
reindex: false,
})

const configDataStore = new DataStore({
coreManager,
namespace: 'config',
batch: async (entries) => {
/** @type {import('multi-core-indexer').Entry[]} */
const entriesToIndex = []
for (const entry of entries) {
const { schemaName } = decodeBlockPrefix(entry.block)
try {
if (schemaName === 'translation') {
const doc = decode(entry.block, {
coreDiscoveryKey: entry.key,
index: entry.index,
})
assert(
doc.schemaName === 'translation',
'expected a translation doc'
)
translationApi.index(doc)
}
entriesToIndex.push(entry)
} catch {
// Ignore errors thrown by values that can't be decoded for now
}
}
const indexed = await indexWriter.batch(entriesToIndex)
return indexed
},
storage: () => new RAM(),
reindex: false,
})

const translationDataType = new DataType({
dataStore: configDataStore,
table: translationTable,
db,
getTranslations: () => {
throw new Error('Cannot get translations for translations')
},
})

const translationApi = new TranslationApi({ dataType: translationDataType })

const dataType = new DataType({
dataStore,
table: observationTable,
db,
getTranslations: translationApi.get.bind(translationApi),
})

return { coreManager, dataType, dataStore, translationApi }
}
93 changes: 93 additions & 0 deletions test/data-type/test-helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import assert from 'node:assert/strict'
import RAM from 'random-access-memory'
import { DataStore } from '../../src/datastore/index.js'
import { DataType } from '../../src/datatype/index.js'
import { IndexWriter } from '../../src/index-writer/index.js'
import { observationTable, translationTable } from '../../src/schema/project.js'
import { createCoreManager } from '../helpers/core-manager.js'

import { decode, decodeBlockPrefix } from '@comapeo/schema'
import Database from 'better-sqlite3'
import { drizzle } from 'drizzle-orm/better-sqlite3'
import { migrate } from 'drizzle-orm/better-sqlite3/migrator'
import TranslationApi from '../../src/translation-api.js'

/**
* @param {object} [opts={}]
* @param {Buffer} [opts.projectKey]
*/
export async function testenv(opts = {}) {
const sqlite = new Database(':memory:')
const db = drizzle(sqlite)
migrate(db, {
migrationsFolder: new URL('../../drizzle/project', import.meta.url)
.pathname,
})

const coreManager = createCoreManager({ ...opts, db })

const indexWriter = new IndexWriter({
tables: [observationTable, translationTable],
sqlite,
})

const dataStore = new DataStore({
coreManager,
namespace: 'data',
batch: async (entries) => indexWriter.batch(entries),
storage: () => new RAM(),
reindex: false,
})

const configDataStore = new DataStore({
coreManager,
namespace: 'config',
batch: async (entries) => {
/** @type {import('multi-core-indexer').Entry[]} */
const entriesToIndex = []
for (const entry of entries) {
const { schemaName } = decodeBlockPrefix(entry.block)
try {
if (schemaName === 'translation') {
const doc = decode(entry.block, {
coreDiscoveryKey: entry.key,
index: entry.index,
})
assert(
doc.schemaName === 'translation',
'expected a translation doc'
)
translationApi.index(doc)
}
entriesToIndex.push(entry)
} catch {
// Ignore errors thrown by values that can't be decoded for now
}
}
const indexed = await indexWriter.batch(entriesToIndex)
return indexed
},
storage: () => new RAM(),
reindex: false,
})

const translationDataType = new DataType({
dataStore: configDataStore,
table: translationTable,
db,
getTranslations: () => {
throw new Error('Cannot get translations for translations')
},
})

const translationApi = new TranslationApi({ dataType: translationDataType })

const dataType = new DataType({
dataStore,
table: observationTable,
db,
getTranslations: translationApi.get.bind(translationApi),
})

return { coreManager, dataType, dataStore, translationApi }
}

0 comments on commit 12b21a2

Please sign in to comment.