From e3411153174098d3b49636954d71436fa3be4546 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Mon, 9 Sep 2019 20:12:38 -0700 Subject: [PATCH] [sharedb] Add Agent class and extraDbs option for ShareDB constructor, loosen query params to any (#38109) * [sharedb] Add Agent class and extraDbs option for ShareDB constructor * Loosen query spec for Connection createFetchQuery and createSubscribeQuery --- types/sharedb/index.d.ts | 38 +++++++++++++++++++++------ types/sharedb/lib/agent.d.ts | 47 ++++++++++++++++++++++++++++++++++ types/sharedb/lib/client.d.ts | 4 +-- types/sharedb/sharedb-tests.ts | 39 +++++++++++++++++++++++++++- 4 files changed, 117 insertions(+), 11 deletions(-) create mode 100644 types/sharedb/lib/agent.d.ts diff --git a/types/sharedb/index.d.ts b/types/sharedb/index.d.ts index 0fc86b0342..0fdd4b5584 100644 --- a/types/sharedb/index.d.ts +++ b/types/sharedb/index.d.ts @@ -3,10 +3,11 @@ // Definitions by: Steve Oney // Eric Hwang // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -// TypeScript Version: 2.1 +// TypeScript Version: 3.0 /// +import Agent = require('./lib/agent'); import * as ShareDB from './lib/sharedb'; interface PubSubOptions { @@ -19,7 +20,17 @@ interface Stream { export = sharedb; declare class sharedb { - constructor(options?: {db?: any, pubsub?: sharedb.PubSub, disableDocAction?: boolean, disableSpaceDelimitedActions?: boolean}); + db: sharedb.DB; + pubsub: sharedb.PubSub; + extraDbs: {[extraDbName: string]: sharedb.ExtraDB}; + + constructor(options?: { + db?: any, + pubsub?: sharedb.PubSub, + extraDbs?: {[extraDbName: string]: sharedb.ExtraDB}, + disableDocAction?: boolean, + disableSpaceDelimitedActions?: boolean + }); connect: (connection?: any, req?: any) => sharedb.Connection; /** * Registers a projection that can be used from clients just like a normal collection. @@ -30,7 +41,7 @@ declare class sharedb { */ addProjection(name: string, collection: string, fields: ProjectionFields): void; listen(stream: any): void; - close(callback?: (err?: Error) => any): void; + close(callback?: BasicCallback): void; /** * Registers a server middleware function. * @@ -50,7 +61,7 @@ declare namespace sharedb { abstract class DB { projectsSnapshots: boolean; disableSubscribe: boolean; - close(callback?: () => void): void; + close(callback?: BasicCallback): void; commit(collection: string, id: string, op: Op, snapshot: any, options: any, callback: (...args: any[]) => any): void; getSnapshot(collection: string, id: string, fields: any, options: any, callback: (...args: any[]) => any): void; getSnapshotBulk(collection: string, ids: string, fields: any, options: any, callback: (...args: any[]) => any): void; @@ -58,15 +69,24 @@ declare namespace sharedb { getOpsToSnapshot(collection: string, id: string, from: number, snapshot: number, options: any, callback: (...args: any[]) => any): void; getOpsBulk(collection: string, fromMap: any, toMap: any, options: any, callback: (...args: any[]) => any): void; getCommittedOpVersion(collection: string, id: string, snapshot: any, op: any, options: any, callback: (...args: any[]) => any): void; - query(collection: string, query: Query, fields: any, options: any, callback: (...args: any[]) => any): void; - queryPoll(collection: string, query: Query, options: any, callback: (...args: any[]) => any): void; - queryPollDoc(collection: string, id: string, query: Query, options: any, callback: (...args: any[]) => any): void; + query: DBQueryMethod; + queryPoll(collection: string, query: any, options: any, callback: (...args: any[]) => any): void; + queryPollDoc(collection: string, id: string, query: any, options: any, callback: (...args: any[]) => any): void; canPollDoc(): boolean; skipPoll(): boolean; } class MemoryDB extends DB { } + // The DBs in `extraDbs` are only ever used for queries, so they don't need the other DB methods. + interface ExtraDB { + query: DBQueryMethod; + close(callback?: BasicCallback): void; + } + + type DBQueryMethod = (collection: string, query: any, fields: ProjectionFields | undefined, options: any, callback: DBQueryCallback) => void; + type DBQueryCallback = (err: Error | null, snapshots: ShareDB.Snapshot[], extra?: any) => void; + abstract class PubSub { private static shallowCopy(obj: any): any; protected prefix?: string; @@ -132,7 +152,7 @@ declare namespace sharedb { interface BaseContext { action: keyof ActionContextMap; - agent: any; + agent: Agent; backend: sharedb; } @@ -220,3 +240,5 @@ interface SubmitRequest { ops: ShareDB.Op[]; channels: string[] | null; } + +type BasicCallback = (err?: Error) => void; diff --git a/types/sharedb/lib/agent.d.ts b/types/sharedb/lib/agent.d.ts new file mode 100644 index 0000000000..42b3a4819d --- /dev/null +++ b/types/sharedb/lib/agent.d.ts @@ -0,0 +1,47 @@ +import { Duplex } from 'stream'; +import { JSONObject } from './sharedb'; +import ShareDbBackend = require('..'); + +export = Agent; + +/** + * An `Agent` is the representation of a client's `Connection` state on the + * server. If the `Connection` was created through `backend.connect` (i.e. the + * client is running in the same Node process as the server), then the `Agent` + * associated with a `Connection` can be accessed through `connection.agent`. + * + * The `Agent` will be made available in all middleware requests. The + * `agent.custom` field is an object that can be used for storing arbitrary + * information for use in middleware. + * + * @see https://github.com/share/sharedb#class-sharedbagent + */ +declare class Agent { + backend: ShareDbBackend; + stream: Duplex & { + /** + * `true` if this is agent is handling a ShareDB client in the same + * Node process. + */ + isServer?: boolean; + }; + /** + * Object for custom use in middleware to store app-specific state for a + * given client session. It is in memory only as long as the session is + * active, and it is passed to each middleware call. + */ + custom: Agent.Custom; + + /** + * Sends a JSON-compatible message to the client for this agent. + * + * @param message + */ + send(message: JSONObject): void; +} + +declare namespace Agent { + interface Custom { + [key: string]: unknown; + } +} diff --git a/types/sharedb/lib/client.d.ts b/types/sharedb/lib/client.d.ts index 08999f2449..f3275b8a19 100644 --- a/types/sharedb/lib/client.d.ts +++ b/types/sharedb/lib/client.d.ts @@ -5,8 +5,8 @@ import * as ShareDB from './sharedb'; export class Connection { constructor(ws: WebSocket | WS); get(collectionName: string, documentID: string): Doc; - createFetchQuery(collectionName: string, query: string, options: {results?: Query[]}, callback: (err: Error, results: any) => any): Query; - createSubscribeQuery(collectionName: string, query: string, options: {results?: Query[]}, callback: (err: Error, results: any) => any): Query; + createFetchQuery(collectionName: string, query: any, options: {results?: Query[]}, callback: (err: Error, results: any[]) => void): Query; + createSubscribeQuery(collectionName: string, query: any, options: {results?: Query[]}, callback: (err: Error, results: any[]) => void): Query; } export type Doc = ShareDB.Doc; export type Query = ShareDB.Query; diff --git a/types/sharedb/sharedb-tests.ts b/types/sharedb/sharedb-tests.ts index d05a651ac0..aabd8c0aec 100644 --- a/types/sharedb/sharedb-tests.ts +++ b/types/sharedb/sharedb-tests.ts @@ -29,15 +29,37 @@ class WebSocketJSONStream extends Duplex { } } -const backend = new ShareDB(); +class CustomExtraDb { + query(collection: string, query: any, fields: unknown, options: {customDbOption: boolean}, callback: ShareDB.DBQueryCallback) { + callback(null, [], {}); + } + close() {} +} + +const backend = new ShareDB({ + extraDbs: {myDb: new CustomExtraDb()} +}); +console.log(backend.db); +console.log(backend.pubsub); +console.log(backend.extraDbs); backend.addProjection('notes_minimal', 'notes', {title: true, creator: true, lastUpdateTime: true}); +// Test module augmentation to attach custom typed properties to `agent.custom`. +import _ShareDbAgent = require('sharedb/lib/agent'); +declare module 'sharedb/lib/agent' { + interface Custom { + user?: {id: string}; + } +} // Exercise middleware (backend.use) type SubmitRelatedActions = 'afterSubmit' | 'apply' | 'commit' | 'submit'; const submitRelatedActions: SubmitRelatedActions[] = ['afterSubmit', 'apply', 'commit', 'submit']; for (const action of submitRelatedActions) { backend.use(action, (request, callback) => { + if (request.agent.custom.user) { + console.log(request.agent.custom.user.id); + } console.log( request.action, request.agent, @@ -56,9 +78,16 @@ for (const action of submitRelatedActions) { }); } backend.use('connect', (context, callback) => { + // `context.req` is the HTTP / websocket request. + // This assumes that some request middleware has handled auth and popuplated `req.userId`. + if (context.req.userId) { + context.agent.custom.user = {id: context.req.userId}; + } console.log( context.action, context.agent, + context.agent.backend === context.backend, + context.agent.stream.isServer, context.backend, context.stream, context.req, @@ -172,4 +201,12 @@ function startClient(callback) { doc.submitOp([{p: ['numClicks'], na: 1}]); callback(); }); + // sharedb-mongo query object + connection.createSubscribeQuery('examples', {numClicks: {$gte: 5}}, null, (err, results) => { + console.log(err, results); + }); + // SQL-ish query adapter that takes a string query condition + connection.createSubscribeQuery('examples', 'numClicks >= 5', null, (err, results) => { + console.log(err, results); + }); }