diff --git a/.gitignore b/.gitignore index 301a6f1..50fb9e4 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ dist node web .log -.npmrc \ No newline at end of file +.npmrc +**/doc \ No newline at end of file diff --git a/packages/client/README.md b/packages/client/README.md index a51eca9..54e4c09 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -49,3 +49,19 @@ To run E2E tests, ensure that [Polybase](https://github.com/polybase/polybase) i ``` yarn test:e2e ``` + +# Doc + +To generate the documentation for the project, run the following command: + +``` + $ npm run doc +``` + +or + +``` + $ yarn run doc +``` + +This will generate the documentation in the `doc` directory. diff --git a/packages/client/package.json b/packages/client/package.json index c62f4db..41714dd 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -17,7 +17,8 @@ "release": "npx np", "test": "jest ./src", "test:e2e": "jest ./e2e --verbose", - "fix": "yarn eslint \"./src/**/*.{ts,tsx}\" --fix" + "fix": "yarn eslint \"./src/**/*.{ts,tsx}\" --fix", + "doc": "npx typedoc --plugin typedoc-plugin-missing-exports" }, "jest": { "preset": "ts-jest", @@ -46,6 +47,8 @@ "terser-webpack-plugin": "^5.3.6", "ts-jest": "^29.1.0", "ts-loader": "^9.4.2", + "typedoc": "^0.24.8", + "typedoc-plugin-missing-exports": "^2.0.1", "typescript": "^4.9.4", "webpack": "^5.75.0", "webpack-cli": "^5.0.1" diff --git a/packages/client/src/Client.ts b/packages/client/src/Client.ts index cfb8793..67bf2a3 100644 --- a/packages/client/src/Client.ts +++ b/packages/client/src/Client.ts @@ -3,13 +3,31 @@ import { AxiosError, AxiosRequestConfig } from 'axios' import { createError, createErrorFromAxiosError } from './errors' import { QueryValue, Request, RequestParams, Sender, SenderResponse, Signer, SignerResponse } from './types' +/** + * The Client configuration. + */ export interface ClientConfig { + /** The unique identifier for this client. + * @example + * ``` + * polybase@ts/client:v0 + * ``` + */ clientId: string + /** The base URL of the Polybase service. + * @example + * ``` + * https://testnet.polybase.xyz/v0 + * ``` + */ baseURL: string } type SignatureCache = Record +/** + * The Client used by the Polybase Client for interacting with the Polybase service. + */ export class Client { private sender: Sender signer?: Signer @@ -23,6 +41,9 @@ export class Client { this.signatureCache = {} } + /** + * Returns a new {@link ClientRequest} instance. + */ request = (req: Request): ClientRequest => { return new ClientRequest(this.sender, { url: req.url, @@ -54,7 +75,12 @@ export class ClientRequest { this.aborter.abort() } - /* Sending a request to the server. */ + /** + * Send a request to the server. + * + * @param withAuth - The authentication mode. + * @param sigExtraTimeMs - Extra time to sign the request. + */ send = async (withAuth: 'none' | 'optional' | 'required', sigExtraTimeMs?: number): Promise> => { try { const req = this.req as AxiosRequestConfig @@ -99,6 +125,7 @@ export class ClientRequest { } } + /** @private */ private getSignature = async (extraTimeMs: number) => { if (!this.signer) return '' @@ -138,6 +165,9 @@ export class ClientRequest { } } +/** + * Parse the given request parameters into a {@link Record} object. + */ export function parseParams(params?: RequestParams): Record { if (!params) return {} return { diff --git a/packages/client/src/Collection.ts b/packages/client/src/Collection.ts index f6945c9..e66252a 100644 --- a/packages/client/src/Collection.ts +++ b/packages/client/src/Collection.ts @@ -1,3 +1,8 @@ +/** + * @see [Collections](https://polybase.xyz/docs/collections) + * @module + */ + import { CollectionRecord, CollectionRecordResponse } from './Record' import { Query, QueryResponse } from './Query' import { Subscription, SubscriptionFn, SubscriptionErrorFn, UnsubscribeFn } from './Subscription' @@ -29,6 +34,9 @@ export class Collection { load = async () => { } + /** + * returns The collection metadata. + */ getMeta = async (): Promise => { if (this.meta) return this.meta // Manually get Collection meta, otherwise we would recursively call this function @@ -44,6 +52,9 @@ export class Collection { return this.meta } + /** + * @returns The prepared AST for this collection. + */ getAST = async (): Promise => { // Return cached value if it exists if (this.astCache) return this.astCache @@ -55,10 +66,19 @@ export class Collection { return collectionAST } + /** + * @returns The short name for this collection. + */ name(): string { return getCollectionShortNameFromId(this.id) } + /** + * Validate the given collection ast. + * + * @param data - the data to be validated + * @returns Whether validation succeeded (`true`) or not (`false`). + */ validate = async (data: Partial) => { const ast = await this.getAST() try { @@ -69,6 +89,10 @@ export class Collection { } } + /** + * @returns Whether this collection can be read pubclicly (`true) or not (`false`) + * @see [Permissions](https://polybase.xyz/docs/permissions) + */ isReadPubliclyAccessible = async (): Promise => { // Without this, we would recursively call this function if (this.id === 'Collection') return true @@ -76,6 +100,11 @@ export class Collection { return this.isCollectionPubliclyAccessible('read') } + /** + * @param methodName - the method (as defined in the collection schema). + * @returns Whether the method can be called (`true`) on this collection or not (`false`). + * @see [Permissions](https://polybase.xyz/docs/permissions) + */ isCallPubliclyAccessible = async (methodName: string) => { // Without this, we would recursively call this function if (this.id === 'Collection') return true @@ -104,6 +133,12 @@ export class Collection { return hasPublicDirective || hasTypeDirective } + /** + * Create a collection record. + * + * @param args - the data for the new collection. + * @returns The newly created collection record. + */ create = async (args: CallArgs = []): Promise> => { if (!Array.isArray(args)) { throw new TypeError('invalid argument: `args` must be an array') @@ -127,6 +162,12 @@ export class Collection { return this.createQuery().get() } + /** + * Retrieves the record, for this collection, with the given id. + * + * @param id - the id of the collection record.:w + * @returns The collection record. + */ record = (id: string): CollectionRecord => { return new CollectionRecord(id, this, this.client, this.onRecordSnapshotRegister) } diff --git a/packages/client/src/Polybase.ts b/packages/client/src/Polybase.ts index eef669d..1345263 100644 --- a/packages/client/src/Polybase.ts +++ b/packages/client/src/Polybase.ts @@ -1,3 +1,13 @@ +/** + *

The Polybase Client SDK.

+ * + *

The Polybase module is how we communicate with the Polybase service. + * @see [Getting Started](https://polybase.xyz/docs/get-started) + *

+ * + * @module + */ + import { parse } from '@polybase/polylang' import axios from 'axios' import { Client } from './Client' @@ -5,9 +15,33 @@ import { Collection } from './Collection' import { PolybaseError, createError } from './errors' import { CollectionMeta, Sender, Signer } from './types' +/** + * Configuration for the Polybase Client. + */ export interface PolybaseConfig { + /** + * The baseURL of the Polybase service. + * @example + * ``` + * https://testnet.polybase.xyz/v0 + * ``` + */ baseURL: string + /** + * The unique identifier of the Client. + * @example + * ``` + * polybase@ts/client:v0 + * ``` + */ clientId: string + /** + * The default namespace for the Client. + * @example + * ``` + * "pk/0x1fda4bead8bc2e85d4de75694b893de5dfb0fbe69e8ed1d2531c805db483ba350ea28d4b1c7acf6171d902586e439a04f23cb0827b08a29cbdf3dd0e5c994ec0/MyClient" + * ``` + */ defaultNamespace?: string sender: Sender signer?: Signer @@ -19,6 +53,9 @@ const defaultConfig = { sender: axios, } +/** + * The Polybase Client. + */ export class Polybase { private config: PolybaseConfig private client: Client @@ -34,6 +71,12 @@ export class Polybase { ) } + /** + * Retrieves the collection with the given path. + * + * @param path - the fully-qualified path to the collection. + * @returns The given {@link Collection} instance. + */ collection(path: string): Collection { const rp = this.getResolvedPath(path) if (this.collections[rp]) return this.collections[rp] @@ -71,7 +114,13 @@ export class Polybase { return this.collection(data.id) } - /* Applies the given schema to the database, creating new collections and adding existing collections */ + /** + * Applies the given schema to the database, creating new collections and adding existing collections. + * + * @param schema: The schema to apply. + * @param namespace: The namespace for the collection. + * @returns An array of {@link Collection} instances. + */ applySchema = async (schema: string, namespace?: string): Promise[]> => { const collections = [] const ns = (namespace ?? this.config.defaultNamespace) diff --git a/packages/client/src/Query.ts b/packages/client/src/Query.ts index 60f3e71..767fcec 100644 --- a/packages/client/src/Query.ts +++ b/packages/client/src/Query.ts @@ -1,3 +1,9 @@ +/** + * @module + * @see [Filter records](https://polybase.xyz/docs/read#filter-records) + * @see [Pagination](https://polybase.xyz/docs/read#pagination) + */ + import { Client } from './Client' import { Collection, QuerySnapshotRegister } from './Collection' import { CollectionRecord, CollectionRecordResponse } from './Record' diff --git a/packages/client/src/Record.ts b/packages/client/src/Record.ts index 88ddfc8..50b28fd 100644 --- a/packages/client/src/Record.ts +++ b/packages/client/src/Record.ts @@ -1,3 +1,9 @@ +/** + * This module contains types and functions for the actual instances of collections. + * + * @module + */ + import { Collection } from './Collection' import { SubscriptionErrorFn, SubscriptionFn } from './Subscription' import { CollectionRecordSnapshotRegister, Request, CallArgs, SenderRawRecordResponse, Block } from './types' @@ -11,6 +17,9 @@ export type CollectionRecordReference = { id: string } +/** + * This represents an instance of a collection. + */ export class CollectionRecord { id: string private collection: Collection @@ -24,6 +33,13 @@ export class CollectionRecord { this.onSnapshotRegister = onSnapshotRegister } + /** + * Calls a function on this collection record. + * The function must be a custom function defined in the collection schema. + * + * @see [Write Data](https://polybase.xyz/docs/write-data) + * @see [Delete Data](https://polybase.xyz/docs/delete-data) + */ call = async (functionName: string, args: CallArgs = []): Promise> => { const ast = await this.collection.getAST() const isCallPubliclyAccessible = await this.collection.isCallPubliclyAccessible(functionName) @@ -39,6 +55,12 @@ export class CollectionRecord { return new CollectionRecordResponse(this.id, res.data, ast, this.collection, this.client, this.onSnapshotRegister) } + /** + * Retrieves the collection record. + * + * @returns The collection recod. + * @see [Read Data](https://polybase.xyz/docs/read) + */ get = async (): Promise> => { const ast = await this.collection.getAST() const isReadPubliclyAccessible = await this.collection.isReadPubliclyAccessible() @@ -65,16 +87,27 @@ export class CollectionRecord { return `record:${this.collection.id}/${this.id}` } + /** + * Listener for updates to this record (after the write is confirmed). + * + * @see [Listen for updates on a record](https://polybase.xyz/docs/read#listen-for-updates-on-a-record) + */ onSnapshot = (fn: SubscriptionFn>, errFn?: SubscriptionErrorFn) => { return this.onSnapshotRegister(this, fn, errFn) } + /** + * The {@link Request} object for this collection record. + */ request = (): Request => ({ url: `/collections/${encodeURIComponent(this.collection.id)}/records/${encodeURIComponent(this.id)}`, method: 'GET', }) } +/** + * The collection record data, as returned by the Polybase service. + */ export class CollectionRecordResponse extends CollectionRecord { data: NT block: Block @@ -100,6 +133,6 @@ export class CollectionRecordResponse extends Collec } /** - * @deprecated use CollectionRecord + * @deprecated use CollectionRecord instead */ export const Doc = CollectionRecord diff --git a/packages/client/src/Subscription.ts b/packages/client/src/Subscription.ts index 87097a9..2f9bf80 100644 --- a/packages/client/src/Subscription.ts +++ b/packages/client/src/Subscription.ts @@ -7,9 +7,13 @@ export type SubscriptionErrorFn = ((err: PolybaseError) => void) export type UnsubscribeFn = (() => void) export interface SubscriptionOptions { - // Default timeout between long poll requests + /** + * Default timeout between long poll requests. + */ timeout: number - // Max timeout after error backoff + /** + * Max timeout after error backoff. + */ maxErrorTimeout: number } diff --git a/packages/client/src/util.ts b/packages/client/src/util.ts index 3032bd8..cc64524 100644 --- a/packages/client/src/util.ts +++ b/packages/client/src/util.ts @@ -1,11 +1,31 @@ +/** + * Utiltiy functions for use by other modules of @polybase/client. + * + * @module + */ + import { CollectionRecord } from './Record' import type { CallArg, FieldTypes } from './types' import { Root as AST, Property as ASTProperty, Collection as ASTCollection, CollectionAttribute, ObjectField } from '@polybase/polylang/dist/ast' +/** + * Retrieve the Collection AST for the given id and ast. + * + * @param id - the collection id. + * @param ast - the ast representing the schema of the collection. + * @returns The prepared Collection AST. + */ export function getCollectionASTFromId(id: string, ast: AST): ASTCollection | undefined { return getCollectionASTFromName(getCollectionShortNameFromId(id), ast) } +/** + * Retrieve the Collection AST for the given name and ast. + * + * @param name - the collection name. + * @param ast - the ast representing the schema of the collection. + * @returns The prepared Collection AST. + */ export function getCollectionASTFromName(name: string, ast: AST): ASTCollection | undefined { const collections = ast.filter((n) => n.kind === 'collection') as ASTCollection[] return collections.find((n: ASTCollection) => n.name === name) @@ -15,16 +35,52 @@ export function getCollectionProperties(collection: ASTCollection): ASTProperty[ return collection.attributes.filter((a: CollectionAttribute) => a.kind === 'property') as ASTProperty[] } +/** + * Extracts the short collection name by removing the namespace prefix. + * + * @example + * ```ts + * const shortName = 'ns/foo' + * console.log(shortName) // displays 'foo' + * ``` + * @param id - the collection id. + * @returns The short collection name. + */ export function getCollectionShortNameFromId(id: string): string { const name = id.split('/').pop() if (!name) throw new Error(`Invalid collection id: ${id}`) return name } +/** + * Encodes the given byte array into a Base64 encoded string. + * + * @example + * ```ts + * const text = 'Hello, world' + * const uint8Array = new Uint8Array(new TextEncoder().encode(text)) + * const base64Encoded = encodeBase64(uint8Array) + * console.log(base64Encoded) // displays 'SGVsbG8sIHdvcmxk' + * ``` + * @param value - the byte arrey. + * @returns The Base64 encoded string. + */ export function encodeBase64(value: Uint8Array): string { return btoa(String.fromCharCode.apply(null, value as unknown as number[])) } +/** + * Decodes the given Base64 encoded string into a byte array. + * + * @example + * ```ts + * const base64Encoded = 'SGVsbG8sIHdvcmxk' + * const decoder = new TextDecoder() + * console.log(decoder.decode(decodeBase64(base64Encoded)) // displays 'Hello, world' + * ``` + * @param value - the byte arrey. + * @returns The Base64 encoded string. + */ export function decodeBase64(value: string): Uint8Array { const binaryString = atob(value) @@ -74,14 +130,25 @@ export function deserializeRecord(data: Record, properties: (ASTPro } } -// Adds a key/value to a record, or creates an object with the key/value +/** + * Adds a key/value to a record, or creates an object with the key/value. + * + * @param key - the entry key. + * @param value - the entry value. + * @returns The updated (or newly-created) record. + */ export function addKeyValue(key: string, value: any, obj?: Record): Record { const o = obj ?? {} o[key] = value return o } -// Removes given keys from the object +/** + * Removes the given keys from the object, if the keys are present in the record. + * + * @param keys - the keys to remove. + * @returns The updated record with the keys removed. + */ export function removeKey(key: string, obj?: any): Record { if (!obj || !isPlainObject(obj)) return {} for (const k of key) { @@ -92,7 +159,9 @@ export function removeKey(key: string, obj?: any): Record { return obj } -// Returns true if the object is a plain object +/** + * Returns true if the object is a plain object. + */ export function isPlainObject(val: any): val is Record { return typeof val === 'object' && val.constructor === Object } diff --git a/packages/client/typedoc.json b/packages/client/typedoc.json new file mode 100644 index 0000000..e714f8e --- /dev/null +++ b/packages/client/typedoc.json @@ -0,0 +1,9 @@ +{ + "entryPoints": [ + "src" + ], + "entryPointStrategy": "expand", + "excludeExternals": true, + "excludePrivate": false, + "out": "./doc" +} \ No newline at end of file