Skip to content

Commit

Permalink
feat: CoreOwnership class w getOwner & getCoreKey
Browse files Browse the repository at this point in the history
- also writeOwnership

Required the addition of an internal `select()` method for dataType,
which directly accesses drizzle.select().from(table)

Cleanup & fix types
  • Loading branch information
gmaclennan committed Aug 28, 2023
1 parent 97b51e4 commit ee4b1e2
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 13 deletions.
34 changes: 29 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@
"hyperdrive": "^11.5.3",
"hyperswarm": "^4.4.1",
"magic-bytes.js": "^1.0.14",
"map-obj": "^5.0.2",
"multi-core-indexer": "^1.0.0-alpha.4",
"multicast-service-discovery": "^4.0.4",
"p-defer": "^4.0.0",
Expand Down
74 changes: 72 additions & 2 deletions src/core-ownership.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,83 @@
import { verifySignature } from '@mapeo/crypto'
import { verifySignature, sign } from '@mapeo/crypto'
import { NAMESPACES } from './core-manager/index.js'
import { parseVersionId } from '@mapeo/schema'
import assert from 'node:assert'
import sodium from 'sodium-universal'
import { kTable, kSelect, kCreateWithDocId } from './datatype/index.js'
import { eq, or } from 'drizzle-orm'
import mapObject from 'map-obj'

/**
* @typedef {Extract<ReturnType<import('@mapeo/schema').decode>, { schemaName: 'coreOwnership' }>} CoreOwnershipWithSignatures
* @typedef {import('./types.js').CoreOwnershipWithSignatures} CoreOwnershipWithSignatures
*/

export class CoreOwnership {
#dataType
/**
*
* @param {object} opts
* @param {import('./datatype/index.js').DataType<import('./datastore/index.js').DataStore<'auth'>, typeof import('./schema/project.js').coreOwnershipTable, 'coreOwnership', import('@mapeo/schema').CoreOwnership, import('@mapeo/schema').CoreOwnershipValue>} opts.dataType
*/
constructor({ dataType }) {
this.#dataType = dataType
}

/**
* @param {string} coreId
* @returns {Promise<string>} deviceId of device that owns the core
*/
async getOwner(coreId) {
const table = this.#dataType[kTable]
const expressions = []
for (const namespace of NAMESPACES) {
expressions.push(eq(table[`${namespace}CoreId`], coreId))
}
// prettier-ignore
const result = this.#dataType[kSelect]()
.where(or.apply(null, expressions))
.get()
if (!result) {
throw new Error('NotFound')
}
return result.docId
}

/**
*
* @param {string} deviceId
* @param {typeof NAMESPACES[number]} namespace
* @returns {Promise<string>} coreId of core belonging to `deviceId` for `namespace`
*/
async getCoreId(deviceId, namespace) {
const result = await this.#dataType.getByDocId(deviceId)
return result[`${namespace}CoreId`]
}

/**
*
* @param {import('./types.js').KeyPair} identityKeypair
* @param {Record<typeof NAMESPACES[number], import('./types.js').KeyPair>} coreKeypairs
*/
async writeOwnership(identityKeypair, coreKeypairs) {
/** @type {import('./types.js').CoreOwnershipWithSignaturesValue} */
const docValue = {
schemaName: 'coreOwnership',
...mapObject(coreKeypairs, (key, value) => {
return [`${key}CoreId`, value.publicKey.toString('hex')]
}),
identitySignature: sign(
identityKeypair.publicKey,
identityKeypair.secretKey
),
coreSignatures: mapObject(coreKeypairs, (key, value) => {
return [key, sign(value.publicKey, value.secretKey)]
}),
}
const docId = identityKeypair.publicKey.toString('hex')
this.#dataType[kCreateWithDocId](docId, docValue)
}
}

/**
* - Validate that the doc is written to the core identified by doc.authCoreId
* - Verify the signatures
Expand Down
31 changes: 26 additions & 5 deletions src/datatype/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@
// `dist` folder at build-time. The types are checked in `test-types/data-types.ts`

import { type MapeoDoc, type MapeoValue } from '@mapeo/schema'
import { type MapeoDocMap, type MapeoValueMap } from '../types.js'
import {
type MapeoDocMap,
type MapeoValueMap,
type CoreOwnershipWithSignatures,
} from '../types.js'
import { type BetterSQLite3Database } from 'drizzle-orm/better-sqlite3'
import { SQLiteSelectBuilder } from 'drizzle-orm/sqlite-core'
import { RunResult } from 'better-sqlite3'

type MapeoDocTableName = `${MapeoDoc['schemaName']}Table`
type GetMapeoDocTables<T> = T[keyof T & MapeoDocTableName]
Expand All @@ -19,9 +26,15 @@ type MapeoDocTablesMap = {
}

export const kCreateWithDocId: unique symbol
export const kSelect: unique symbol
export const kTable: unique symbol

type OmitUnion<T, K extends keyof any> = T extends any ? Omit<T, K> : never

// We do this because we can't pass a generic to this (an "indexed access type")
// https://stackoverflow.com/a/75792683/3794085
declare const from: SQLiteSelectBuilder<undefined, 'sync', RunResult>['from']

export class DataType<
TDataStore extends import('../datastore/index.js').DataStore,
TTable extends MapeoDocTables,
Expand All @@ -41,10 +54,18 @@ export class DataType<
getPermissions?: () => any
})

[kCreateWithDocId]<T extends import('type-fest').Exact<TValue, T>>(
docId: string,
value: T
): Promise<TDoc & { forks: string[] }>
get [kTable](): TTable

[kCreateWithDocId]<
T extends import('type-fest').Exact<
// For this we use the "internal" version of CoreOwnership, with signatures
| Exclude<TValue, { schemaName: 'coreOwnership' }>
| CoreOwnershipWithSignaturesValue,
T
>
>(docId: string, value: T): Promise<TDoc & { forks: string[] }>

[kSelect](): ReturnType<typeof from<TTable>>

create<T extends import('type-fest').Exact<TValue, T>>(
value: T
Expand Down
15 changes: 15 additions & 0 deletions src/datatype/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ function generateDate() {
return new Date().toISOString()
}
export const kCreateWithDocId = Symbol('kCreateWithDocId')
export const kSelect = Symbol('select')
export const kTable = Symbol('table')

/**
* @template {import('../datastore/index.js').DataStore} TDataStore
Expand All @@ -58,6 +60,7 @@ export class DataType {
#getPermissions
#schemaName
#sql
#db

/**
*
Expand All @@ -72,6 +75,7 @@ export class DataType {
this.#table = table
this.#schemaName = /** @type {TSchemaName} */ (getTableConfig(table).name)
this.#getPermissions = getPermissions
this.#db = db
this.#sql = {
getByDocId: db
.select()
Expand All @@ -82,6 +86,10 @@ export class DataType {
}
}

get [kTable]() {
return this.#table
}

/**
* @template {import('type-fest').Exact<TValue, T>} T
* @param {T} value
Expand Down Expand Up @@ -175,6 +183,13 @@ export class DataType {
return this.getByDocId(docId)
}

/**
* @param {Parameters<import('drizzle-orm/better-sqlite3').BetterSQLite3Database['select']>[0]} fields
*/
async [kSelect](fields) {
return this.#db.select(fields).from(this.#table)
}

/**
* Validate that existing docs with the given versionIds (links):
* - exist
Expand Down
5 changes: 5 additions & 0 deletions src/schema/project.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ export const observationTable = sqliteTable(
)
export const presetTable = sqliteTable('preset', toColumns(schemas.preset))
export const fieldTable = sqliteTable('field', toColumns(schemas.field))
export const coreOwnershipTable = sqliteTable(
'coreOwnership',
toColumns(schemas.coreOwnership)
)

export const observationBacklinkTable = backlinkTable(observationTable)
export const presetBacklinkTable = backlinkTable(presetTable)
export const fieldBacklinkTable = backlinkTable(fieldTable)
export const coreOwnershipBacklinkTable = backlinkTable(coreOwnershipTable)
12 changes: 11 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type {
SetOptional,
} from 'type-fest'
import { SUPPORTED_BLOB_VARIANTS } from './blob-store/index.js'
import { MapeoDoc, MapeoValue } from '@mapeo/schema'
import { MapeoCommon, MapeoDoc, MapeoValue, decode } from '@mapeo/schema'
import type Protomux from 'protomux'
import type NoiseStream from '@hyperswarm/secret-stream'
import { Duplex } from 'streamx'
Expand Down Expand Up @@ -62,6 +62,16 @@ export type MapeoValueMap = {
[K in MapeoValue['schemaName']]: Extract<MapeoValue, { schemaName: K }>
}

// TODO: Replace this with exports from @mapeo/schema
export type CoreOwnershipWithSignatures = Extract<
ReturnType<typeof decode>,
{ schemaName: 'coreOwnership' }
>
export type CoreOwnershipWithSignaturesValue = Omit<
CoreOwnershipWithSignatures,
Exclude<keyof MapeoCommon, 'schemaName'>
>

type NullToOptional<T> = SetOptional<T, NullKeys<T>>
type RemoveNull<T> = {
[K in keyof T]: Exclude<T[K], null>
Expand Down

0 comments on commit ee4b1e2

Please sign in to comment.