From ad6bbb669f20dafc2377424aef6308018368f0d6 Mon Sep 17 00:00:00 2001 From: Paul Le Cam Date: Mon, 15 Apr 2024 18:43:59 +0100 Subject: [PATCH] Create list and set writers --- composites/points/src/index.ts | 6 +++ libraries/points/src/base-reader.ts | 60 ++++++++++++++++++++++++++ libraries/points/src/ceramic.ts | 12 ++++++ libraries/points/src/list-writer.ts | 43 ++++++++++++++++++ libraries/points/src/set-reader.ts | 33 ++++++++++++++ libraries/points/src/set-writer.ts | 67 +++++++++++++++++++++++++++++ 6 files changed, 221 insertions(+) create mode 100644 libraries/points/src/base-reader.ts create mode 100644 libraries/points/src/list-writer.ts create mode 100644 libraries/points/src/set-reader.ts create mode 100644 libraries/points/src/set-writer.ts diff --git a/composites/points/src/index.ts b/composites/points/src/index.ts index ec2c1f7..76d5beb 100644 --- a/composites/points/src/index.ts +++ b/composites/points/src/index.ts @@ -7,3 +7,9 @@ import { definition } from './definition.js' export { definition } export const SimplePointsAggregationID = definition.models.SimplePointsAggregation.id export const SimplePointsAllocationID = definition.models.SimplePointsAllocation.id + +export type PointsContent = { + issuer: string // DID + recipient: string // DID + points: number +} diff --git a/libraries/points/src/base-reader.ts b/libraries/points/src/base-reader.ts new file mode 100644 index 0000000..a5a0b50 --- /dev/null +++ b/libraries/points/src/base-reader.ts @@ -0,0 +1,60 @@ +import type { BaseQuery } from '@ceramicnetwork/common' +import { DocumentLoader } from '@composedb/loader' +import type { CeramicAPI } from '@composedb/types' +import type { PointsContent } from '@ceramic-solutions/points-composite' + +import { getCeramic } from './ceramic.js' +import { getQueryForRecipient, queryConnection } from './query.js' +import type { QueryDocumentsOptions, QueryDocumentsResult } from './types.js' + +export type PointsBaseReaderParams = { + issuer: string + modelID: string + ceramic?: CeramicAPI | string + loader?: DocumentLoader +} + +export class PointsBaseReader { + #baseQuery: BaseQuery + #issuer: string + #ceramic: CeramicAPI + #loader: DocumentLoader + #modelID: string + + constructor(params: PointsBaseReaderParams) { + const ceramic = getCeramic(params.ceramic) + this.#baseQuery = { account: params.issuer, models: [params.modelID] } + this.#modelID = params.modelID + this.#ceramic = ceramic + this.#issuer = params.issuer + this.#loader = params.loader ?? new DocumentLoader({ ceramic }) + } + + get issuer(): string { + return this.#issuer + } + + get modelID(): string { + return this.#modelID + } + + get ceramic(): CeramicAPI { + return this.#ceramic + } + + get loader(): DocumentLoader { + return this.#loader + } + + async queryDocuments(options?: QueryDocumentsOptions): Promise> { + return await queryConnection(this.#loader, this.#baseQuery, options) + } + + async queryDocumentsFor( + did: string, + options?: QueryDocumentsOptions, + ): Promise> { + const query = getQueryForRecipient(this.#baseQuery, did) + return await queryConnection(this.#loader, query, options) + } +} diff --git a/libraries/points/src/ceramic.ts b/libraries/points/src/ceramic.ts index da50ecb..9cb7cad 100644 --- a/libraries/points/src/ceramic.ts +++ b/libraries/points/src/ceramic.ts @@ -1,6 +1,18 @@ import { CeramicClient } from '@ceramicnetwork/http-client' import type { CeramicAPI } from '@composedb/types' +import { getAuthenticatedDID } from './did.js' + export function getCeramic(ceramic?: CeramicAPI | string): CeramicAPI { return ceramic == null || typeof ceramic === 'string' ? new CeramicClient(ceramic) : ceramic } + +export async function getAuthenticatedCeramic( + seed: Uint8Array, + ceramicClientOrURL?: CeramicAPI | string, +): Promise { + const ceramic = getCeramic(ceramicClientOrURL) + const did = await getAuthenticatedDID(seed) + ceramic.did = did + return ceramic +} diff --git a/libraries/points/src/list-writer.ts b/libraries/points/src/list-writer.ts new file mode 100644 index 0000000..6a78ad8 --- /dev/null +++ b/libraries/points/src/list-writer.ts @@ -0,0 +1,43 @@ +import type { CeramicAPI, ModelInstanceDocument } from '@composedb/types' +import type { PointsContent } from '@ceramic-solutions/points-composite' + +import { getAuthenticatedCeramic } from './ceramic.js' +import { PointsBaseReader, type PointsBaseReaderParams } from './base-reader.js' + +export type PointsListWriterFromSeedParams = PointsBaseReaderParams & { + seed: Uint8Array +} + +export type PointsListWriterParams = Omit & { + ceramic: CeramicAPI +} + +export class PointsListWriter< + Content extends PointsContent = PointsContent, +> extends PointsBaseReader { + static async fromSeed( + params: PointsListWriterFromSeedParams, + ): Promise> { + const ceramic = await getAuthenticatedCeramic(params.seed, params.ceramic) + return new PointsListWriter({ ...params, ceramic }) + } + + constructor(params: PointsListWriterParams) { + if (!params.ceramic.did?.authenticated) { + throw new Error(`An authenticated DID instance must be set on the Ceramic client`) + } + super({ ...params, issuer: params.ceramic.did.id }) + } + + async createDocument(content: Content): Promise> { + return await this.loader.create(this.modelID, content) + } + + async removeDocument(id: string): Promise { + const doc = await this.loader.load({ id }) + if (doc.metadata.model.toString() !== this.modelID) { + throw new Error(`Document ${id} is not using the expected model ${this.modelID}`) + } + await doc.shouldIndex(false) + } +} diff --git a/libraries/points/src/set-reader.ts b/libraries/points/src/set-reader.ts new file mode 100644 index 0000000..6d0a909 --- /dev/null +++ b/libraries/points/src/set-reader.ts @@ -0,0 +1,33 @@ +import type { DeterministicLoadOptions } from '@composedb/loader' +import type { ModelInstanceDocument } from '@composedb/types' +import type { PointsContent } from '@ceramic-solutions/points-composite' + +import { PointsBaseReader, type PointsBaseReaderParams } from './base-reader.js' + +export function toUniqueArg(value: string | Array): Array { + return Array.isArray(value) ? value : [value] +} + +export type PointsSetReaderParams = PointsBaseReaderParams + +export class PointsSetReader< + Content extends PointsContent = PointsContent, +> extends PointsBaseReader { + async loadDocumentFor( + didOrValues: string | Array, + options: DeterministicLoadOptions = {}, + ): Promise | null> { + return await this.loader.loadSet(this.issuer, this.modelID, toUniqueArg(didOrValues), { + ignoreEmpty: true, + ...options, + }) + } + + async loadPointsFor( + didOrValues: string | Array, + options?: DeterministicLoadOptions, + ): Promise { + const doc = await this.loadDocumentFor(didOrValues, options) + return doc?.content?.points ?? 0 + } +} diff --git a/libraries/points/src/set-writer.ts b/libraries/points/src/set-writer.ts new file mode 100644 index 0000000..df447bd --- /dev/null +++ b/libraries/points/src/set-writer.ts @@ -0,0 +1,67 @@ +import type { CeramicAPI, ModelInstanceDocument } from '@composedb/types' +import type { PointsContent } from '@ceramic-solutions/points-composite' + +import { getAuthenticatedCeramic } from './ceramic.js' +import { PointsSetReader, type PointsSetReaderParams, toUniqueArg } from './set-reader.js' + +export type PointsSetWriterFromSeedParams = PointsSetReaderParams & { + seed: Uint8Array +} + +export type PointsSetWriterParams = Omit & { + ceramic: CeramicAPI +} + +export class PointsSetWriter< + Content extends PointsContent = PointsContent, +> extends PointsSetReader { + static async fromSeed( + params: PointsSetWriterFromSeedParams, + ): Promise> { + const ceramic = await getAuthenticatedCeramic(params.seed, params.ceramic) + return new PointsSetWriter({ ...params, ceramic }) + } + + constructor(params: PointsSetWriterParams) { + if (!params.ceramic.did?.authenticated) { + throw new Error(`An authenticated DID instance must be set on the Ceramic client`) + } + super({ ...params, issuer: params.ceramic.did.id }) + } + + async _loadDocumentFor( + didOrValues: string | Array, + ): Promise> { + const doc = await this.loadDocumentFor(didOrValues, { ignoreEmpty: false, onlyIndexed: false }) + return doc! + } + + async setDocumentFor( + didOrValues: string | Array, + updateContent: (content: Content | null) => Partial, + ): Promise> { + const unique = toUniqueArg(didOrValues) + const doc = await this._loadDocumentFor(didOrValues) + const content = doc!.content + await doc!.replace({ + // Copy existing content or set recipient (assuming it's the first value) + ...(content ?? { recipient: unique[0] }), + // Apply content update + ...updateContent(content), + } as Content) + return doc! + } + + async removeDocument(id: string): Promise { + const doc = await this.loader.load({ id }) + if (doc.metadata.model.toString() !== this.modelID) { + throw new Error(`Document ${id} is not using the expected model ${this.modelID}`) + } + await doc.shouldIndex(false) + } + + async removeDocumentFor(didOrValues: string | Array): Promise { + const doc = await this._loadDocumentFor(didOrValues) + await doc.shouldIndex(false) + } +}