From 22d039a229e3ef08a793e1c98b473b1b8e18ac5e Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Tue, 8 Oct 2024 19:16:09 +0200 Subject: [PATCH] :sparkles: Ozone sets (#2636) * :sparkles: Initial implementation of sets api on ozone * :sparkles: Introduce sortDirection to querySets * :broom: Cleanup and refactor * :sparkles: Align setView for response * :recycle: Rename and add specific error * :bug: Cleanup unnecessary check that is covered by lexicon * :sparkles: Rename remove to delete and add set suffix * :sparkles: Use id and createdAt for values pagination * :sparkles: Add index on createdAt for query perf and other cleanups * :bug: Set createdAt when inserting values * :memo: Add changeset * :sparkles: Add index on setId and createdAt --- .changeset/modern-snails-flash.md | 6 + lexicons/tools/ozone/set/addValues.json | 32 ++ lexicons/tools/ozone/set/defs.json | 49 +++ lexicons/tools/ozone/set/deleteSet.json | 36 +++ lexicons/tools/ozone/set/deleteValues.json | 37 +++ lexicons/tools/ozone/set/getValues.json | 56 ++++ lexicons/tools/ozone/set/querySets.json | 57 ++++ lexicons/tools/ozone/set/upsertSet.json | 24 ++ packages/api/src/client/index.ts | 83 +++++ packages/api/src/client/lexicons.ts | 301 ++++++++++++++++++ .../client/types/tools/ozone/set/addValues.ts | 34 ++ .../src/client/types/tools/ozone/set/defs.ts | 44 +++ .../client/types/tools/ozone/set/deleteSet.ts | 47 +++ .../types/tools/ozone/set/deleteValues.ts | 44 +++ .../client/types/tools/ozone/set/getValues.ts | 49 +++ .../client/types/tools/ozone/set/querySets.ts | 41 +++ .../client/types/tools/ozone/set/upsertSet.ts | 31 ++ packages/ozone/src/api/index.ts | 12 + packages/ozone/src/api/set/addValues.ts | 28 ++ packages/ozone/src/api/set/deleteSet.ts | 34 ++ packages/ozone/src/api/set/deleteValues.ts | 31 ++ packages/ozone/src/api/set/getValues.ts | 42 +++ packages/ozone/src/api/set/querySets.ts | 36 +++ packages/ozone/src/api/set/upsertSet.ts | 38 +++ packages/ozone/src/context.ts | 8 + .../db/migrations/20241008T205730722Z-sets.ts | 53 +++ packages/ozone/src/db/migrations/index.ts | 1 + packages/ozone/src/db/pagination.ts | 2 +- packages/ozone/src/db/schema/index.ts | 2 + packages/ozone/src/db/schema/ozone_set.ts | 24 ++ packages/ozone/src/lexicon/index.ts | 82 +++++ packages/ozone/src/lexicon/lexicons.ts | 301 ++++++++++++++++++ .../types/tools/ozone/set/addValues.ts | 41 +++ .../src/lexicon/types/tools/ozone/set/defs.ts | 44 +++ .../types/tools/ozone/set/deleteSet.ts | 50 +++ .../types/tools/ozone/set/deleteValues.ts | 42 +++ .../types/tools/ozone/set/getValues.ts | 51 +++ .../types/tools/ozone/set/querySets.ts | 52 +++ .../types/tools/ozone/set/upsertSet.ts | 43 +++ packages/ozone/src/set/service.ts | 227 +++++++++++++ .../tests/__snapshots__/sets.test.ts.snap | 46 +++ packages/ozone/tests/sets.test.ts | 246 ++++++++++++++ packages/pds/src/lexicon/index.ts | 82 +++++ packages/pds/src/lexicon/lexicons.ts | 301 ++++++++++++++++++ .../types/tools/ozone/set/addValues.ts | 41 +++ .../src/lexicon/types/tools/ozone/set/defs.ts | 44 +++ .../types/tools/ozone/set/deleteSet.ts | 50 +++ .../types/tools/ozone/set/deleteValues.ts | 42 +++ .../types/tools/ozone/set/getValues.ts | 51 +++ .../types/tools/ozone/set/querySets.ts | 52 +++ .../types/tools/ozone/set/upsertSet.ts | 43 +++ 51 files changed, 3212 insertions(+), 1 deletion(-) create mode 100644 .changeset/modern-snails-flash.md create mode 100644 lexicons/tools/ozone/set/addValues.json create mode 100644 lexicons/tools/ozone/set/defs.json create mode 100644 lexicons/tools/ozone/set/deleteSet.json create mode 100644 lexicons/tools/ozone/set/deleteValues.json create mode 100644 lexicons/tools/ozone/set/getValues.json create mode 100644 lexicons/tools/ozone/set/querySets.json create mode 100644 lexicons/tools/ozone/set/upsertSet.json create mode 100644 packages/api/src/client/types/tools/ozone/set/addValues.ts create mode 100644 packages/api/src/client/types/tools/ozone/set/defs.ts create mode 100644 packages/api/src/client/types/tools/ozone/set/deleteSet.ts create mode 100644 packages/api/src/client/types/tools/ozone/set/deleteValues.ts create mode 100644 packages/api/src/client/types/tools/ozone/set/getValues.ts create mode 100644 packages/api/src/client/types/tools/ozone/set/querySets.ts create mode 100644 packages/api/src/client/types/tools/ozone/set/upsertSet.ts create mode 100644 packages/ozone/src/api/set/addValues.ts create mode 100644 packages/ozone/src/api/set/deleteSet.ts create mode 100644 packages/ozone/src/api/set/deleteValues.ts create mode 100644 packages/ozone/src/api/set/getValues.ts create mode 100644 packages/ozone/src/api/set/querySets.ts create mode 100644 packages/ozone/src/api/set/upsertSet.ts create mode 100644 packages/ozone/src/db/migrations/20241008T205730722Z-sets.ts create mode 100644 packages/ozone/src/db/schema/ozone_set.ts create mode 100644 packages/ozone/src/lexicon/types/tools/ozone/set/addValues.ts create mode 100644 packages/ozone/src/lexicon/types/tools/ozone/set/defs.ts create mode 100644 packages/ozone/src/lexicon/types/tools/ozone/set/deleteSet.ts create mode 100644 packages/ozone/src/lexicon/types/tools/ozone/set/deleteValues.ts create mode 100644 packages/ozone/src/lexicon/types/tools/ozone/set/getValues.ts create mode 100644 packages/ozone/src/lexicon/types/tools/ozone/set/querySets.ts create mode 100644 packages/ozone/src/lexicon/types/tools/ozone/set/upsertSet.ts create mode 100644 packages/ozone/src/set/service.ts create mode 100644 packages/ozone/tests/__snapshots__/sets.test.ts.snap create mode 100644 packages/ozone/tests/sets.test.ts create mode 100644 packages/pds/src/lexicon/types/tools/ozone/set/addValues.ts create mode 100644 packages/pds/src/lexicon/types/tools/ozone/set/defs.ts create mode 100644 packages/pds/src/lexicon/types/tools/ozone/set/deleteSet.ts create mode 100644 packages/pds/src/lexicon/types/tools/ozone/set/deleteValues.ts create mode 100644 packages/pds/src/lexicon/types/tools/ozone/set/getValues.ts create mode 100644 packages/pds/src/lexicon/types/tools/ozone/set/querySets.ts create mode 100644 packages/pds/src/lexicon/types/tools/ozone/set/upsertSet.ts diff --git a/.changeset/modern-snails-flash.md b/.changeset/modern-snails-flash.md new file mode 100644 index 00000000000..d41dec616a7 --- /dev/null +++ b/.changeset/modern-snails-flash.md @@ -0,0 +1,6 @@ +--- +"@atproto/ozone": patch +"@atproto/api": patch +--- + +Sets api to manage lists of strings on ozone, mostly aimed for automod configuration diff --git a/lexicons/tools/ozone/set/addValues.json b/lexicons/tools/ozone/set/addValues.json new file mode 100644 index 00000000000..345df5db0bb --- /dev/null +++ b/lexicons/tools/ozone/set/addValues.json @@ -0,0 +1,32 @@ +{ + "lexicon": 1, + "id": "tools.ozone.set.addValues", + "defs": { + "main": { + "type": "procedure", + "description": "Add values to a specific set. Attempting to add values to a set that does not exist will result in an error.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["name", "values"], + "properties": { + "name": { + "type": "string", + "description": "Name of the set to add values to" + }, + "values": { + "type": "array", + "minLength": 1, + "maxLength": 1000, + "items": { + "type": "string" + }, + "description": "Array of string values to add to the set" + } + } + } + } + } + } +} diff --git a/lexicons/tools/ozone/set/defs.json b/lexicons/tools/ozone/set/defs.json new file mode 100644 index 00000000000..4ad69521f19 --- /dev/null +++ b/lexicons/tools/ozone/set/defs.json @@ -0,0 +1,49 @@ +{ + "lexicon": 1, + "id": "tools.ozone.set.defs", + "defs": { + "set": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string", + "minLength": 3, + "maxLength": 128 + }, + "description": { + "type": "string", + "maxGraphemes": 1024, + "maxLength": 10240 + } + } + }, + "setView": { + "type": "object", + "required": ["name", "setSize", "createdAt", "updatedAt"], + "properties": { + "name": { + "type": "string", + "minLength": 3, + "maxLength": 128 + }, + "description": { + "type": "string", + "maxGraphemes": 1024, + "maxLength": 10240 + }, + "setSize": { + "type": "integer" + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "updatedAt": { + "type": "string", + "format": "datetime" + } + } + } + } +} diff --git a/lexicons/tools/ozone/set/deleteSet.json b/lexicons/tools/ozone/set/deleteSet.json new file mode 100644 index 00000000000..70e6000380c --- /dev/null +++ b/lexicons/tools/ozone/set/deleteSet.json @@ -0,0 +1,36 @@ +{ + "lexicon": 1, + "id": "tools.ozone.set.deleteSet", + "defs": { + "main": { + "type": "procedure", + "description": "Delete an entire set. Attempting to delete a set that does not exist will result in an error.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string", + "description": "Name of the set to delete" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "properties": {} + } + }, + "errors": [ + { + "name": "SetNotFound", + "description": "set with the given name does not exist" + } + ] + } + } +} diff --git a/lexicons/tools/ozone/set/deleteValues.json b/lexicons/tools/ozone/set/deleteValues.json new file mode 100644 index 00000000000..e40b8d44d89 --- /dev/null +++ b/lexicons/tools/ozone/set/deleteValues.json @@ -0,0 +1,37 @@ +{ + "lexicon": 1, + "id": "tools.ozone.set.deleteValues", + "defs": { + "main": { + "type": "procedure", + "description": "Delete values from a specific set. Attempting to delete values that are not in the set will not result in an error", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["name", "values"], + "properties": { + "name": { + "type": "string", + "description": "Name of the set to delete values from" + }, + "values": { + "type": "array", + "minLength": 1, + "items": { + "type": "string" + }, + "description": "Array of string values to delete from the set" + } + } + } + }, + "errors": [ + { + "name": "SetNotFound", + "description": "set with the given name does not exist" + } + ] + } + } +} diff --git a/lexicons/tools/ozone/set/getValues.json b/lexicons/tools/ozone/set/getValues.json new file mode 100644 index 00000000000..dca2fb275e0 --- /dev/null +++ b/lexicons/tools/ozone/set/getValues.json @@ -0,0 +1,56 @@ +{ + "lexicon": 1, + "id": "tools.ozone.set.getValues", + "defs": { + "main": { + "type": "query", + "description": "Get a specific set and its values", + "parameters": { + "type": "params", + "required": ["name"], + "properties": { + "name": { + "type": "string" + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 1000, + "default": 100 + }, + "cursor": { + "type": "string" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["set", "values"], + "properties": { + "set": { + "type": "ref", + "ref": "tools.ozone.set.defs#setView" + }, + "values": { + "type": "array", + "items": { + "type": "string" + } + }, + "cursor": { + "type": "string" + } + } + } + }, + "errors": [ + { + "name": "SetNotFound", + "description": "set with the given name does not exist" + } + ] + } + } +} diff --git a/lexicons/tools/ozone/set/querySets.json b/lexicons/tools/ozone/set/querySets.json new file mode 100644 index 00000000000..d4bedb005c4 --- /dev/null +++ b/lexicons/tools/ozone/set/querySets.json @@ -0,0 +1,57 @@ +{ + "lexicon": 1, + "id": "tools.ozone.set.querySets", + "defs": { + "main": { + "type": "query", + "description": "Query available sets", + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "cursor": { + "type": "string" + }, + "namePrefix": { + "type": "string" + }, + "sortBy": { + "type": "string", + "enum": ["name", "createdAt", "updatedAt"], + "default": "name" + }, + "sortDirection": { + "type": "string", + "default": "asc", + "enum": ["asc", "desc"], + "description": "Defaults to ascending order of name field." + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["sets"], + "properties": { + "sets": { + "type": "array", + "items": { + "type": "ref", + "ref": "tools.ozone.set.defs#setView" + } + }, + "cursor": { + "type": "string" + } + } + } + } + } + } +} diff --git a/lexicons/tools/ozone/set/upsertSet.json b/lexicons/tools/ozone/set/upsertSet.json new file mode 100644 index 00000000000..43ae0f9523c --- /dev/null +++ b/lexicons/tools/ozone/set/upsertSet.json @@ -0,0 +1,24 @@ +{ + "lexicon": 1, + "id": "tools.ozone.set.upsertSet", + "defs": { + "main": { + "type": "procedure", + "description": "Create or update set metadata", + "input": { + "encoding": "application/json", + "schema": { + "type": "ref", + "ref": "tools.ozone.set.defs#set" + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "ref", + "ref": "tools.ozone.set.defs#setView" + } + } + } + } +} diff --git a/packages/api/src/client/index.ts b/packages/api/src/client/index.ts index ef839da17d8..b7fc4f1f45d 100644 --- a/packages/api/src/client/index.ts +++ b/packages/api/src/client/index.ts @@ -206,6 +206,13 @@ import * as ToolsOzoneModerationQueryEvents from './types/tools/ozone/moderation import * as ToolsOzoneModerationQueryStatuses from './types/tools/ozone/moderation/queryStatuses' import * as ToolsOzoneModerationSearchRepos from './types/tools/ozone/moderation/searchRepos' import * as ToolsOzoneServerGetConfig from './types/tools/ozone/server/getConfig' +import * as ToolsOzoneSetAddValues from './types/tools/ozone/set/addValues' +import * as ToolsOzoneSetDefs from './types/tools/ozone/set/defs' +import * as ToolsOzoneSetDeleteSet from './types/tools/ozone/set/deleteSet' +import * as ToolsOzoneSetDeleteValues from './types/tools/ozone/set/deleteValues' +import * as ToolsOzoneSetGetValues from './types/tools/ozone/set/getValues' +import * as ToolsOzoneSetQuerySets from './types/tools/ozone/set/querySets' +import * as ToolsOzoneSetUpsertSet from './types/tools/ozone/set/upsertSet' import * as ToolsOzoneSignatureDefs from './types/tools/ozone/signature/defs' import * as ToolsOzoneSignatureFindCorrelation from './types/tools/ozone/signature/findCorrelation' import * as ToolsOzoneSignatureFindRelatedAccounts from './types/tools/ozone/signature/findRelatedAccounts' @@ -418,6 +425,13 @@ export * as ToolsOzoneModerationQueryEvents from './types/tools/ozone/moderation export * as ToolsOzoneModerationQueryStatuses from './types/tools/ozone/moderation/queryStatuses' export * as ToolsOzoneModerationSearchRepos from './types/tools/ozone/moderation/searchRepos' export * as ToolsOzoneServerGetConfig from './types/tools/ozone/server/getConfig' +export * as ToolsOzoneSetAddValues from './types/tools/ozone/set/addValues' +export * as ToolsOzoneSetDefs from './types/tools/ozone/set/defs' +export * as ToolsOzoneSetDeleteSet from './types/tools/ozone/set/deleteSet' +export * as ToolsOzoneSetDeleteValues from './types/tools/ozone/set/deleteValues' +export * as ToolsOzoneSetGetValues from './types/tools/ozone/set/getValues' +export * as ToolsOzoneSetQuerySets from './types/tools/ozone/set/querySets' +export * as ToolsOzoneSetUpsertSet from './types/tools/ozone/set/upsertSet' export * as ToolsOzoneSignatureDefs from './types/tools/ozone/signature/defs' export * as ToolsOzoneSignatureFindCorrelation from './types/tools/ozone/signature/findCorrelation' export * as ToolsOzoneSignatureFindRelatedAccounts from './types/tools/ozone/signature/findRelatedAccounts' @@ -3404,6 +3418,7 @@ export class ToolsOzoneNS { communication: ToolsOzoneCommunicationNS moderation: ToolsOzoneModerationNS server: ToolsOzoneServerNS + set: ToolsOzoneSetNS signature: ToolsOzoneSignatureNS team: ToolsOzoneTeamNS @@ -3412,6 +3427,7 @@ export class ToolsOzoneNS { this.communication = new ToolsOzoneCommunicationNS(client) this.moderation = new ToolsOzoneModerationNS(client) this.server = new ToolsOzoneServerNS(client) + this.set = new ToolsOzoneSetNS(client) this.signature = new ToolsOzoneSignatureNS(client) this.team = new ToolsOzoneTeamNS(client) } @@ -3604,6 +3620,73 @@ export class ToolsOzoneServerNS { } } +export class ToolsOzoneSetNS { + _client: XrpcClient + + constructor(client: XrpcClient) { + this._client = client + } + + addValues( + data?: ToolsOzoneSetAddValues.InputSchema, + opts?: ToolsOzoneSetAddValues.CallOptions, + ): Promise { + return this._client.call('tools.ozone.set.addValues', opts?.qp, data, opts) + } + + deleteSet( + data?: ToolsOzoneSetDeleteSet.InputSchema, + opts?: ToolsOzoneSetDeleteSet.CallOptions, + ): Promise { + return this._client + .call('tools.ozone.set.deleteSet', opts?.qp, data, opts) + .catch((e) => { + throw ToolsOzoneSetDeleteSet.toKnownErr(e) + }) + } + + deleteValues( + data?: ToolsOzoneSetDeleteValues.InputSchema, + opts?: ToolsOzoneSetDeleteValues.CallOptions, + ): Promise { + return this._client + .call('tools.ozone.set.deleteValues', opts?.qp, data, opts) + .catch((e) => { + throw ToolsOzoneSetDeleteValues.toKnownErr(e) + }) + } + + getValues( + params?: ToolsOzoneSetGetValues.QueryParams, + opts?: ToolsOzoneSetGetValues.CallOptions, + ): Promise { + return this._client + .call('tools.ozone.set.getValues', params, undefined, opts) + .catch((e) => { + throw ToolsOzoneSetGetValues.toKnownErr(e) + }) + } + + querySets( + params?: ToolsOzoneSetQuerySets.QueryParams, + opts?: ToolsOzoneSetQuerySets.CallOptions, + ): Promise { + return this._client.call( + 'tools.ozone.set.querySets', + params, + undefined, + opts, + ) + } + + upsertSet( + data?: ToolsOzoneSetUpsertSet.InputSchema, + opts?: ToolsOzoneSetUpsertSet.CallOptions, + ): Promise { + return this._client.call('tools.ozone.set.upsertSet', opts?.qp, data, opts) + } +} + export class ToolsOzoneSignatureNS { _client: XrpcClient diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 7b373bca624..2b9f298ad96 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -12139,6 +12139,300 @@ export const schemaDict = { }, }, }, + ToolsOzoneSetAddValues: { + lexicon: 1, + id: 'tools.ozone.set.addValues', + defs: { + main: { + type: 'procedure', + description: + 'Add values to a specific set. Attempting to add values to a set that does not exist will result in an error.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['name', 'values'], + properties: { + name: { + type: 'string', + description: 'Name of the set to add values to', + }, + values: { + type: 'array', + minLength: 1, + maxLength: 1000, + items: { + type: 'string', + }, + description: 'Array of string values to add to the set', + }, + }, + }, + }, + }, + }, + }, + ToolsOzoneSetDefs: { + lexicon: 1, + id: 'tools.ozone.set.defs', + defs: { + set: { + type: 'object', + required: ['name'], + properties: { + name: { + type: 'string', + minLength: 3, + maxLength: 128, + }, + description: { + type: 'string', + maxGraphemes: 1024, + maxLength: 10240, + }, + }, + }, + setView: { + type: 'object', + required: ['name', 'setSize', 'createdAt', 'updatedAt'], + properties: { + name: { + type: 'string', + minLength: 3, + maxLength: 128, + }, + description: { + type: 'string', + maxGraphemes: 1024, + maxLength: 10240, + }, + setSize: { + type: 'integer', + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + updatedAt: { + type: 'string', + format: 'datetime', + }, + }, + }, + }, + }, + ToolsOzoneSetDeleteSet: { + lexicon: 1, + id: 'tools.ozone.set.deleteSet', + defs: { + main: { + type: 'procedure', + description: + 'Delete an entire set. Attempting to delete a set that does not exist will result in an error.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['name'], + properties: { + name: { + type: 'string', + description: 'Name of the set to delete', + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + properties: {}, + }, + }, + errors: [ + { + name: 'SetNotFound', + description: 'set with the given name does not exist', + }, + ], + }, + }, + }, + ToolsOzoneSetDeleteValues: { + lexicon: 1, + id: 'tools.ozone.set.deleteValues', + defs: { + main: { + type: 'procedure', + description: + 'Delete values from a specific set. Attempting to delete values that are not in the set will not result in an error', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['name', 'values'], + properties: { + name: { + type: 'string', + description: 'Name of the set to delete values from', + }, + values: { + type: 'array', + minLength: 1, + items: { + type: 'string', + }, + description: 'Array of string values to delete from the set', + }, + }, + }, + }, + errors: [ + { + name: 'SetNotFound', + description: 'set with the given name does not exist', + }, + ], + }, + }, + }, + ToolsOzoneSetGetValues: { + lexicon: 1, + id: 'tools.ozone.set.getValues', + defs: { + main: { + type: 'query', + description: 'Get a specific set and its values', + parameters: { + type: 'params', + required: ['name'], + properties: { + name: { + type: 'string', + }, + limit: { + type: 'integer', + minimum: 1, + maximum: 1000, + default: 100, + }, + cursor: { + type: 'string', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['set', 'values'], + properties: { + set: { + type: 'ref', + ref: 'lex:tools.ozone.set.defs#setView', + }, + values: { + type: 'array', + items: { + type: 'string', + }, + }, + cursor: { + type: 'string', + }, + }, + }, + }, + errors: [ + { + name: 'SetNotFound', + description: 'set with the given name does not exist', + }, + ], + }, + }, + }, + ToolsOzoneSetQuerySets: { + lexicon: 1, + id: 'tools.ozone.set.querySets', + defs: { + main: { + type: 'query', + description: 'Query available sets', + parameters: { + type: 'params', + properties: { + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 50, + }, + cursor: { + type: 'string', + }, + namePrefix: { + type: 'string', + }, + sortBy: { + type: 'string', + enum: ['name', 'createdAt', 'updatedAt'], + default: 'name', + }, + sortDirection: { + type: 'string', + default: 'asc', + enum: ['asc', 'desc'], + description: 'Defaults to ascending order of name field.', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['sets'], + properties: { + sets: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:tools.ozone.set.defs#setView', + }, + }, + cursor: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + ToolsOzoneSetUpsertSet: { + lexicon: 1, + id: 'tools.ozone.set.upsertSet', + defs: { + main: { + type: 'procedure', + description: 'Create or update set metadata', + input: { + encoding: 'application/json', + schema: { + type: 'ref', + ref: 'lex:tools.ozone.set.defs#set', + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'ref', + ref: 'lex:tools.ozone.set.defs#setView', + }, + }, + }, + }, + }, ToolsOzoneSignatureDefs: { lexicon: 1, id: 'tools.ozone.signature.defs', @@ -12764,6 +13058,13 @@ export const ids = { ToolsOzoneModerationQueryStatuses: 'tools.ozone.moderation.queryStatuses', ToolsOzoneModerationSearchRepos: 'tools.ozone.moderation.searchRepos', ToolsOzoneServerGetConfig: 'tools.ozone.server.getConfig', + ToolsOzoneSetAddValues: 'tools.ozone.set.addValues', + ToolsOzoneSetDefs: 'tools.ozone.set.defs', + ToolsOzoneSetDeleteSet: 'tools.ozone.set.deleteSet', + ToolsOzoneSetDeleteValues: 'tools.ozone.set.deleteValues', + ToolsOzoneSetGetValues: 'tools.ozone.set.getValues', + ToolsOzoneSetQuerySets: 'tools.ozone.set.querySets', + ToolsOzoneSetUpsertSet: 'tools.ozone.set.upsertSet', ToolsOzoneSignatureDefs: 'tools.ozone.signature.defs', ToolsOzoneSignatureFindCorrelation: 'tools.ozone.signature.findCorrelation', ToolsOzoneSignatureFindRelatedAccounts: diff --git a/packages/api/src/client/types/tools/ozone/set/addValues.ts b/packages/api/src/client/types/tools/ozone/set/addValues.ts new file mode 100644 index 00000000000..04379b2e5c3 --- /dev/null +++ b/packages/api/src/client/types/tools/ozone/set/addValues.ts @@ -0,0 +1,34 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { HeadersMap, XRPCError } from '@atproto/xrpc' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' + +export interface QueryParams {} + +export interface InputSchema { + /** Name of the set to add values to */ + name: string + /** Array of string values to add to the set */ + values: string[] + [k: string]: unknown +} + +export interface CallOptions { + signal?: AbortSignal + headers?: HeadersMap + qp?: QueryParams + encoding?: 'application/json' +} + +export interface Response { + success: boolean + headers: HeadersMap +} + +export function toKnownErr(e: any) { + return e +} diff --git a/packages/api/src/client/types/tools/ozone/set/defs.ts b/packages/api/src/client/types/tools/ozone/set/defs.ts new file mode 100644 index 00000000000..d6823eb9b84 --- /dev/null +++ b/packages/api/src/client/types/tools/ozone/set/defs.ts @@ -0,0 +1,44 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' + +export interface Set { + name: string + description?: string + [k: string]: unknown +} + +export function isSet(v: unknown): v is Set { + return ( + isObj(v) && hasProp(v, '$type') && v.$type === 'tools.ozone.set.defs#set' + ) +} + +export function validateSet(v: unknown): ValidationResult { + return lexicons.validate('tools.ozone.set.defs#set', v) +} + +export interface SetView { + name: string + description?: string + setSize: number + createdAt: string + updatedAt: string + [k: string]: unknown +} + +export function isSetView(v: unknown): v is SetView { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'tools.ozone.set.defs#setView' + ) +} + +export function validateSetView(v: unknown): ValidationResult { + return lexicons.validate('tools.ozone.set.defs#setView', v) +} diff --git a/packages/api/src/client/types/tools/ozone/set/deleteSet.ts b/packages/api/src/client/types/tools/ozone/set/deleteSet.ts new file mode 100644 index 00000000000..52988ca54c9 --- /dev/null +++ b/packages/api/src/client/types/tools/ozone/set/deleteSet.ts @@ -0,0 +1,47 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { HeadersMap, XRPCError } from '@atproto/xrpc' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' + +export interface QueryParams {} + +export interface InputSchema { + /** Name of the set to delete */ + name: string + [k: string]: unknown +} + +export interface OutputSchema { + [k: string]: unknown +} + +export interface CallOptions { + signal?: AbortSignal + headers?: HeadersMap + qp?: QueryParams + encoding?: 'application/json' +} + +export interface Response { + success: boolean + headers: HeadersMap + data: OutputSchema +} + +export class SetNotFoundError extends XRPCError { + constructor(src: XRPCError) { + super(src.status, src.error, src.message, src.headers, { cause: src }) + } +} + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + if (e.error === 'SetNotFound') return new SetNotFoundError(e) + } + + return e +} diff --git a/packages/api/src/client/types/tools/ozone/set/deleteValues.ts b/packages/api/src/client/types/tools/ozone/set/deleteValues.ts new file mode 100644 index 00000000000..71845188b04 --- /dev/null +++ b/packages/api/src/client/types/tools/ozone/set/deleteValues.ts @@ -0,0 +1,44 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { HeadersMap, XRPCError } from '@atproto/xrpc' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' + +export interface QueryParams {} + +export interface InputSchema { + /** Name of the set to delete values from */ + name: string + /** Array of string values to delete from the set */ + values: string[] + [k: string]: unknown +} + +export interface CallOptions { + signal?: AbortSignal + headers?: HeadersMap + qp?: QueryParams + encoding?: 'application/json' +} + +export interface Response { + success: boolean + headers: HeadersMap +} + +export class SetNotFoundError extends XRPCError { + constructor(src: XRPCError) { + super(src.status, src.error, src.message, src.headers, { cause: src }) + } +} + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + if (e.error === 'SetNotFound') return new SetNotFoundError(e) + } + + return e +} diff --git a/packages/api/src/client/types/tools/ozone/set/getValues.ts b/packages/api/src/client/types/tools/ozone/set/getValues.ts new file mode 100644 index 00000000000..e8b848a5ef7 --- /dev/null +++ b/packages/api/src/client/types/tools/ozone/set/getValues.ts @@ -0,0 +1,49 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { HeadersMap, XRPCError } from '@atproto/xrpc' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' +import * as ToolsOzoneSetDefs from './defs' + +export interface QueryParams { + name: string + limit?: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + set: ToolsOzoneSetDefs.SetView + values: string[] + cursor?: string + [k: string]: unknown +} + +export interface CallOptions { + signal?: AbortSignal + headers?: HeadersMap +} + +export interface Response { + success: boolean + headers: HeadersMap + data: OutputSchema +} + +export class SetNotFoundError extends XRPCError { + constructor(src: XRPCError) { + super(src.status, src.error, src.message, src.headers, { cause: src }) + } +} + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + if (e.error === 'SetNotFound') return new SetNotFoundError(e) + } + + return e +} diff --git a/packages/api/src/client/types/tools/ozone/set/querySets.ts b/packages/api/src/client/types/tools/ozone/set/querySets.ts new file mode 100644 index 00000000000..dcb19d52890 --- /dev/null +++ b/packages/api/src/client/types/tools/ozone/set/querySets.ts @@ -0,0 +1,41 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { HeadersMap, XRPCError } from '@atproto/xrpc' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' +import * as ToolsOzoneSetDefs from './defs' + +export interface QueryParams { + limit?: number + cursor?: string + namePrefix?: string + sortBy?: 'name' | 'createdAt' | 'updatedAt' + /** Defaults to ascending order of name field. */ + sortDirection?: 'asc' | 'desc' +} + +export type InputSchema = undefined + +export interface OutputSchema { + sets: ToolsOzoneSetDefs.SetView[] + cursor?: string + [k: string]: unknown +} + +export interface CallOptions { + signal?: AbortSignal + headers?: HeadersMap +} + +export interface Response { + success: boolean + headers: HeadersMap + data: OutputSchema +} + +export function toKnownErr(e: any) { + return e +} diff --git a/packages/api/src/client/types/tools/ozone/set/upsertSet.ts b/packages/api/src/client/types/tools/ozone/set/upsertSet.ts new file mode 100644 index 00000000000..3489fe4cff1 --- /dev/null +++ b/packages/api/src/client/types/tools/ozone/set/upsertSet.ts @@ -0,0 +1,31 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { HeadersMap, XRPCError } from '@atproto/xrpc' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' +import * as ToolsOzoneSetDefs from './defs' + +export interface QueryParams {} + +export type InputSchema = ToolsOzoneSetDefs.Set +export type OutputSchema = ToolsOzoneSetDefs.SetView + +export interface CallOptions { + signal?: AbortSignal + headers?: HeadersMap + qp?: QueryParams + encoding?: 'application/json' +} + +export interface Response { + success: boolean + headers: HeadersMap + data: OutputSchema +} + +export function toKnownErr(e: any) { + return e +} diff --git a/packages/ozone/src/api/index.ts b/packages/ozone/src/api/index.ts index d75353ecea7..650569eaf6f 100644 --- a/packages/ozone/src/api/index.ts +++ b/packages/ozone/src/api/index.ts @@ -23,6 +23,12 @@ import listMembers from './team/listMembers' import getConfig from './server/getConfig' import chat from './chat' import proxied from './proxied' +import setAddValues from './set/addValues' +import setGetValues from './set/getValues' +import querySets from './set/querySets' +import upsertSet from './set/upsertSet' +import setDeleteValues from './set/deleteValues' +import deleteSet from './set/deleteSet' import getRepos from './moderation/getRepos' export * as health from './health' @@ -54,5 +60,11 @@ export default function (server: Server, ctx: AppContext) { chat(server, ctx) proxied(server, ctx) getConfig(server, ctx) + setAddValues(server, ctx) + setGetValues(server, ctx) + querySets(server, ctx) + upsertSet(server, ctx) + setDeleteValues(server, ctx) + deleteSet(server, ctx) return server } diff --git a/packages/ozone/src/api/set/addValues.ts b/packages/ozone/src/api/set/addValues.ts new file mode 100644 index 00000000000..92f6580472c --- /dev/null +++ b/packages/ozone/src/api/set/addValues.ts @@ -0,0 +1,28 @@ +import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' +import { Server } from '../../lexicon' +import AppContext from '../../context' + +export default function (server: Server, ctx: AppContext) { + server.tools.ozone.set.addValues({ + auth: ctx.authVerifier.modOrAdminToken, + handler: async ({ input, auth }) => { + const access = auth.credentials + const db = ctx.db + const { name, values } = input.body + + if (!access.isModerator) { + throw new AuthRequiredError( + 'Must be a moderator to add values to a set', + ) + } + + const setService = ctx.setService(db) + const set = await setService.getByName(name) + if (!set) { + throw new InvalidRequestError(`Set with name "${name}" does not exist`) + } + + await setService.addValues(set.id, values) + }, + }) +} diff --git a/packages/ozone/src/api/set/deleteSet.ts b/packages/ozone/src/api/set/deleteSet.ts new file mode 100644 index 00000000000..e6de898f047 --- /dev/null +++ b/packages/ozone/src/api/set/deleteSet.ts @@ -0,0 +1,34 @@ +import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' +import { Server } from '../../lexicon' +import AppContext from '../../context' + +export default function (server: Server, ctx: AppContext) { + server.tools.ozone.set.deleteSet({ + auth: ctx.authVerifier.modOrAdminToken, + handler: async ({ input, auth }) => { + const access = auth.credentials + const db = ctx.db + const { name } = input.body + + if (!access.isModerator) { + throw new AuthRequiredError('Must be a moderator to delete a set') + } + + const setService = ctx.setService(db) + const set = await setService.getByName(name) + if (!set) { + throw new InvalidRequestError( + `Set with name "${name}" does not exist`, + 'SetNotFound', + ) + } + + await setService.removeSet(set.id) + + return { + encoding: 'application/json', + body: {}, + } + }, + }) +} diff --git a/packages/ozone/src/api/set/deleteValues.ts b/packages/ozone/src/api/set/deleteValues.ts new file mode 100644 index 00000000000..d76eac05ecb --- /dev/null +++ b/packages/ozone/src/api/set/deleteValues.ts @@ -0,0 +1,31 @@ +import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' +import { Server } from '../../lexicon' +import AppContext from '../../context' + +export default function (server: Server, ctx: AppContext) { + server.tools.ozone.set.deleteValues({ + auth: ctx.authVerifier.modOrAdminToken, + handler: async ({ input, auth }) => { + const access = auth.credentials + const db = ctx.db + const { name, values } = input.body + + if (!access.isModerator) { + throw new AuthRequiredError( + 'Must be a moderator to remove values from a set', + ) + } + + const setService = ctx.setService(db) + const set = await setService.getByName(name) + if (!set) { + throw new InvalidRequestError( + `Set with name "${name}" does not exist`, + 'SetNotFound', + ) + } + + await setService.removeValues(set.id, values) + }, + }) +} diff --git a/packages/ozone/src/api/set/getValues.ts b/packages/ozone/src/api/set/getValues.ts new file mode 100644 index 00000000000..4ca1878f101 --- /dev/null +++ b/packages/ozone/src/api/set/getValues.ts @@ -0,0 +1,42 @@ +import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' +import { Server } from '../../lexicon' +import AppContext from '../../context' + +export default function (server: Server, ctx: AppContext) { + server.tools.ozone.set.getValues({ + auth: ctx.authVerifier.modOrAdminToken, + handler: async ({ params, auth }) => { + const access = auth.credentials + const db = ctx.db + const { name, limit, cursor } = params + + if (!access.isModerator) { + throw new AuthRequiredError('Must be a moderator to get set details') + } + + const setService = ctx.setService(db) + + const result = await setService.getSetWithValues({ + name, + limit, + cursor, + }) + + if (!result) { + throw new InvalidRequestError( + `Set with name "${name}" not found`, + 'SetNotFound', + ) + } + + return { + encoding: 'application/json', + body: { + set: setService.view(result.set), + values: result.values, + cursor: result.cursor, + }, + } + }, + }) +} diff --git a/packages/ozone/src/api/set/querySets.ts b/packages/ozone/src/api/set/querySets.ts new file mode 100644 index 00000000000..303cea2066f --- /dev/null +++ b/packages/ozone/src/api/set/querySets.ts @@ -0,0 +1,36 @@ +import { AuthRequiredError } from '@atproto/xrpc-server' +import { Server } from '../../lexicon' +import AppContext from '../../context' + +export default function (server: Server, ctx: AppContext) { + server.tools.ozone.set.querySets({ + auth: ctx.authVerifier.modOrAdminToken, + handler: async ({ params, auth }) => { + const access = auth.credentials + const db = ctx.db + const { limit, cursor, namePrefix, sortBy, sortDirection } = params + + if (!access.isModerator) { + throw new AuthRequiredError('Must be a moderator to query sets') + } + + const setService = ctx.setService(db) + + const queryResult = await setService.query({ + limit, + cursor, + namePrefix, + sortBy, + sortDirection, + }) + + return { + encoding: 'application/json', + body: { + sets: queryResult.sets.map((set) => setService.view(set)), + cursor: queryResult.cursor, + }, + } + }, + }) +} diff --git a/packages/ozone/src/api/set/upsertSet.ts b/packages/ozone/src/api/set/upsertSet.ts new file mode 100644 index 00000000000..572736f08b2 --- /dev/null +++ b/packages/ozone/src/api/set/upsertSet.ts @@ -0,0 +1,38 @@ +import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' +import { Server } from '../../lexicon' +import AppContext from '../../context' + +export default function (server: Server, ctx: AppContext) { + server.tools.ozone.set.upsertSet({ + auth: ctx.authVerifier.modOrAdminToken, + handler: async ({ input, auth }) => { + const access = auth.credentials + const db = ctx.db + const { name, description } = input.body + + if (!access.isModerator) { + throw new AuthRequiredError( + 'Must be a moderator to create or update a set', + ) + } + + const setService = ctx.setService(db) + + await setService.upsert({ + name, + description: description ?? null, + }) + const setWithSize = await setService.getByNameWithSize(name) + + // Unlikely to happen since we just upserted the set + if (!setWithSize) { + throw new InvalidRequestError(`Set not found`) + } + + return { + encoding: 'application/json', + body: setService.view(setWithSize), + } + }, + }) +} diff --git a/packages/ozone/src/context.ts b/packages/ozone/src/context.ts index c120adce5f3..7739b900bb4 100644 --- a/packages/ozone/src/context.ts +++ b/packages/ozone/src/context.ts @@ -26,12 +26,14 @@ import { ParsedLabelers, parseLabelerHeader, } from './util' +import { SetService, SetServiceCreator } from './set/service' export type AppContextOptions = { db: Database cfg: OzoneConfig modService: ModerationServiceCreator communicationTemplateService: CommunicationTemplateServiceCreator + setService: SetServiceCreator teamService: TeamServiceCreator appviewAgent: AtpAgent pdsAgent: AtpAgent | undefined @@ -117,6 +119,7 @@ export class AppContext { const communicationTemplateService = CommunicationTemplateService.creator() const teamService = TeamService.creator() + const setService = SetService.creator() const sequencer = new Sequencer(modService(db)) @@ -133,6 +136,7 @@ export class AppContext { modService, communicationTemplateService, teamService, + setService, appviewAgent, pdsAgent, chatAgent, @@ -182,6 +186,10 @@ export class AppContext { return this.opts.teamService } + get setService(): SetServiceCreator { + return this.opts.setService + } + get appviewAgent(): AtpAgent { return this.opts.appviewAgent } diff --git a/packages/ozone/src/db/migrations/20241008T205730722Z-sets.ts b/packages/ozone/src/db/migrations/20241008T205730722Z-sets.ts new file mode 100644 index 00000000000..b11d07546a7 --- /dev/null +++ b/packages/ozone/src/db/migrations/20241008T205730722Z-sets.ts @@ -0,0 +1,53 @@ +import { Kysely, sql } from 'kysely' + +export async function up(db: Kysely): Promise { + // Create the sets table + await db.schema + .createTable('set_detail') + .addColumn('id', 'serial', (col) => col.primaryKey()) + .addColumn('name', 'varchar', (col) => col.notNull().unique()) + .addColumn('description', 'varchar') + .addColumn('createdAt', 'timestamptz', (col) => + col.defaultTo(sql`now()`).notNull(), + ) + .addColumn('updatedAt', 'timestamptz', (col) => + col.defaultTo(sql`now()`).notNull(), + ) + .execute() + + // Create the set values table + await db.schema + .createTable('set_value') + .addColumn('id', 'bigserial', (col) => col.primaryKey()) + .addColumn('setId', 'integer', (col) => + col.notNull().references('set_detail.id'), + ) + .addColumn('value', 'varchar', (col) => col.notNull()) + .addColumn('createdAt', 'timestamptz', (col) => + col.defaultTo(sql`now()`).notNull(), + ) + .execute() + + // Add indexes for better performance + await db.schema + .createIndex('set_detail_name_idx') + .on('set_detail') + .column('name') + .execute() + + // Create a unique constraint on setId and value + await db.schema + .alterTable('set_value') + .addUniqueConstraint('set_value_setid_value_unique', ['setId', 'value']) + .execute() + await db.schema + .createIndex('set_value_setid_created_at_idx') + .on('set_value') + .columns(['setId', 'createdAt']) + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema.dropTable('set_value').execute() + await db.schema.dropTable('set_detail').execute() +} diff --git a/packages/ozone/src/db/migrations/index.ts b/packages/ozone/src/db/migrations/index.ts index 658f5bc71da..db85bad7ad4 100644 --- a/packages/ozone/src/db/migrations/index.ts +++ b/packages/ozone/src/db/migrations/index.ts @@ -14,3 +14,4 @@ export * as _20240814T003647759Z from './20240814T003647759Z-event-created-at-in export * as _20240903T205730722Z from './20240903T205730722Z-add-template-lang' export * as _20240904T205730722Z from './20240904T205730722Z-add-subject-did-index' export * as _20241001T205730722Z from './20241001T205730722Z-subject-status-review-state-index' +export * as _20241008T205730722Z from './20241008T205730722Z-sets' diff --git a/packages/ozone/src/db/pagination.ts b/packages/ozone/src/db/pagination.ts index 5f3d935582d..9149a42f1c1 100644 --- a/packages/ozone/src/db/pagination.ts +++ b/packages/ozone/src/db/pagination.ts @@ -146,7 +146,7 @@ export class StatusKeyset extends GenericKeyset { type TimeIdKeysetParam = { id: number - createdAt: string + createdAt: string | Date } type TimeIdResult = TimeIdKeysetParam diff --git a/packages/ozone/src/db/schema/index.ts b/packages/ozone/src/db/schema/index.ts index bd3fa9fa5fa..c7c0bf81e3b 100644 --- a/packages/ozone/src/db/schema/index.ts +++ b/packages/ozone/src/db/schema/index.ts @@ -7,6 +7,7 @@ import * as blobPushEvent from './blob_push_event' import * as label from './label' import * as signingKey from './signing_key' import * as communicationTemplate from './communication_template' +import * as set from './ozone_set' import * as member from './member' export type DatabaseSchemaType = modEvent.PartialDB & @@ -17,6 +18,7 @@ export type DatabaseSchemaType = modEvent.PartialDB & recordPushEvent.PartialDB & blobPushEvent.PartialDB & communicationTemplate.PartialDB & + set.PartialDB & member.PartialDB export type DatabaseSchema = Kysely diff --git a/packages/ozone/src/db/schema/ozone_set.ts b/packages/ozone/src/db/schema/ozone_set.ts new file mode 100644 index 00000000000..42ff440fa9b --- /dev/null +++ b/packages/ozone/src/db/schema/ozone_set.ts @@ -0,0 +1,24 @@ +import { Generated, GeneratedAlways } from 'kysely' + +export const ozoneSetTableName = 'set_detail' +export const ozoneSetValueTableName = 'set_value' + +export interface SetDetail { + id: GeneratedAlways + name: string + description: string | null + createdAt: Generated + updatedAt: Generated +} + +export interface SetValue { + id: GeneratedAlways + setId: number + value: string + createdAt: Generated +} + +export type PartialDB = { + [ozoneSetTableName]: SetDetail + [ozoneSetValueTableName]: SetValue +} diff --git a/packages/ozone/src/lexicon/index.ts b/packages/ozone/src/lexicon/index.ts index 97c1a3b9f35..d2edd584b1a 100644 --- a/packages/ozone/src/lexicon/index.ts +++ b/packages/ozone/src/lexicon/index.ts @@ -173,6 +173,12 @@ import * as ToolsOzoneModerationQueryEvents from './types/tools/ozone/moderation import * as ToolsOzoneModerationQueryStatuses from './types/tools/ozone/moderation/queryStatuses' import * as ToolsOzoneModerationSearchRepos from './types/tools/ozone/moderation/searchRepos' import * as ToolsOzoneServerGetConfig from './types/tools/ozone/server/getConfig' +import * as ToolsOzoneSetAddValues from './types/tools/ozone/set/addValues' +import * as ToolsOzoneSetDeleteSet from './types/tools/ozone/set/deleteSet' +import * as ToolsOzoneSetDeleteValues from './types/tools/ozone/set/deleteValues' +import * as ToolsOzoneSetGetValues from './types/tools/ozone/set/getValues' +import * as ToolsOzoneSetQuerySets from './types/tools/ozone/set/querySets' +import * as ToolsOzoneSetUpsertSet from './types/tools/ozone/set/upsertSet' import * as ToolsOzoneSignatureFindCorrelation from './types/tools/ozone/signature/findCorrelation' import * as ToolsOzoneSignatureFindRelatedAccounts from './types/tools/ozone/signature/findRelatedAccounts' import * as ToolsOzoneSignatureSearchAccounts from './types/tools/ozone/signature/searchAccounts' @@ -2164,6 +2170,7 @@ export class ToolsOzoneNS { communication: ToolsOzoneCommunicationNS moderation: ToolsOzoneModerationNS server: ToolsOzoneServerNS + set: ToolsOzoneSetNS signature: ToolsOzoneSignatureNS team: ToolsOzoneTeamNS @@ -2172,6 +2179,7 @@ export class ToolsOzoneNS { this.communication = new ToolsOzoneCommunicationNS(server) this.moderation = new ToolsOzoneModerationNS(server) this.server = new ToolsOzoneServerNS(server) + this.set = new ToolsOzoneSetNS(server) this.signature = new ToolsOzoneSignatureNS(server) this.team = new ToolsOzoneTeamNS(server) } @@ -2355,6 +2363,80 @@ export class ToolsOzoneServerNS { } } +export class ToolsOzoneSetNS { + _server: Server + + constructor(server: Server) { + this._server = server + } + + addValues( + cfg: ConfigOf< + AV, + ToolsOzoneSetAddValues.Handler>, + ToolsOzoneSetAddValues.HandlerReqCtx> + >, + ) { + const nsid = 'tools.ozone.set.addValues' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + deleteSet( + cfg: ConfigOf< + AV, + ToolsOzoneSetDeleteSet.Handler>, + ToolsOzoneSetDeleteSet.HandlerReqCtx> + >, + ) { + const nsid = 'tools.ozone.set.deleteSet' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + deleteValues( + cfg: ConfigOf< + AV, + ToolsOzoneSetDeleteValues.Handler>, + ToolsOzoneSetDeleteValues.HandlerReqCtx> + >, + ) { + const nsid = 'tools.ozone.set.deleteValues' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getValues( + cfg: ConfigOf< + AV, + ToolsOzoneSetGetValues.Handler>, + ToolsOzoneSetGetValues.HandlerReqCtx> + >, + ) { + const nsid = 'tools.ozone.set.getValues' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + querySets( + cfg: ConfigOf< + AV, + ToolsOzoneSetQuerySets.Handler>, + ToolsOzoneSetQuerySets.HandlerReqCtx> + >, + ) { + const nsid = 'tools.ozone.set.querySets' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + upsertSet( + cfg: ConfigOf< + AV, + ToolsOzoneSetUpsertSet.Handler>, + ToolsOzoneSetUpsertSet.HandlerReqCtx> + >, + ) { + const nsid = 'tools.ozone.set.upsertSet' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } +} + export class ToolsOzoneSignatureNS { _server: Server diff --git a/packages/ozone/src/lexicon/lexicons.ts b/packages/ozone/src/lexicon/lexicons.ts index 7b373bca624..2b9f298ad96 100644 --- a/packages/ozone/src/lexicon/lexicons.ts +++ b/packages/ozone/src/lexicon/lexicons.ts @@ -12139,6 +12139,300 @@ export const schemaDict = { }, }, }, + ToolsOzoneSetAddValues: { + lexicon: 1, + id: 'tools.ozone.set.addValues', + defs: { + main: { + type: 'procedure', + description: + 'Add values to a specific set. Attempting to add values to a set that does not exist will result in an error.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['name', 'values'], + properties: { + name: { + type: 'string', + description: 'Name of the set to add values to', + }, + values: { + type: 'array', + minLength: 1, + maxLength: 1000, + items: { + type: 'string', + }, + description: 'Array of string values to add to the set', + }, + }, + }, + }, + }, + }, + }, + ToolsOzoneSetDefs: { + lexicon: 1, + id: 'tools.ozone.set.defs', + defs: { + set: { + type: 'object', + required: ['name'], + properties: { + name: { + type: 'string', + minLength: 3, + maxLength: 128, + }, + description: { + type: 'string', + maxGraphemes: 1024, + maxLength: 10240, + }, + }, + }, + setView: { + type: 'object', + required: ['name', 'setSize', 'createdAt', 'updatedAt'], + properties: { + name: { + type: 'string', + minLength: 3, + maxLength: 128, + }, + description: { + type: 'string', + maxGraphemes: 1024, + maxLength: 10240, + }, + setSize: { + type: 'integer', + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + updatedAt: { + type: 'string', + format: 'datetime', + }, + }, + }, + }, + }, + ToolsOzoneSetDeleteSet: { + lexicon: 1, + id: 'tools.ozone.set.deleteSet', + defs: { + main: { + type: 'procedure', + description: + 'Delete an entire set. Attempting to delete a set that does not exist will result in an error.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['name'], + properties: { + name: { + type: 'string', + description: 'Name of the set to delete', + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + properties: {}, + }, + }, + errors: [ + { + name: 'SetNotFound', + description: 'set with the given name does not exist', + }, + ], + }, + }, + }, + ToolsOzoneSetDeleteValues: { + lexicon: 1, + id: 'tools.ozone.set.deleteValues', + defs: { + main: { + type: 'procedure', + description: + 'Delete values from a specific set. Attempting to delete values that are not in the set will not result in an error', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['name', 'values'], + properties: { + name: { + type: 'string', + description: 'Name of the set to delete values from', + }, + values: { + type: 'array', + minLength: 1, + items: { + type: 'string', + }, + description: 'Array of string values to delete from the set', + }, + }, + }, + }, + errors: [ + { + name: 'SetNotFound', + description: 'set with the given name does not exist', + }, + ], + }, + }, + }, + ToolsOzoneSetGetValues: { + lexicon: 1, + id: 'tools.ozone.set.getValues', + defs: { + main: { + type: 'query', + description: 'Get a specific set and its values', + parameters: { + type: 'params', + required: ['name'], + properties: { + name: { + type: 'string', + }, + limit: { + type: 'integer', + minimum: 1, + maximum: 1000, + default: 100, + }, + cursor: { + type: 'string', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['set', 'values'], + properties: { + set: { + type: 'ref', + ref: 'lex:tools.ozone.set.defs#setView', + }, + values: { + type: 'array', + items: { + type: 'string', + }, + }, + cursor: { + type: 'string', + }, + }, + }, + }, + errors: [ + { + name: 'SetNotFound', + description: 'set with the given name does not exist', + }, + ], + }, + }, + }, + ToolsOzoneSetQuerySets: { + lexicon: 1, + id: 'tools.ozone.set.querySets', + defs: { + main: { + type: 'query', + description: 'Query available sets', + parameters: { + type: 'params', + properties: { + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 50, + }, + cursor: { + type: 'string', + }, + namePrefix: { + type: 'string', + }, + sortBy: { + type: 'string', + enum: ['name', 'createdAt', 'updatedAt'], + default: 'name', + }, + sortDirection: { + type: 'string', + default: 'asc', + enum: ['asc', 'desc'], + description: 'Defaults to ascending order of name field.', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['sets'], + properties: { + sets: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:tools.ozone.set.defs#setView', + }, + }, + cursor: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + ToolsOzoneSetUpsertSet: { + lexicon: 1, + id: 'tools.ozone.set.upsertSet', + defs: { + main: { + type: 'procedure', + description: 'Create or update set metadata', + input: { + encoding: 'application/json', + schema: { + type: 'ref', + ref: 'lex:tools.ozone.set.defs#set', + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'ref', + ref: 'lex:tools.ozone.set.defs#setView', + }, + }, + }, + }, + }, ToolsOzoneSignatureDefs: { lexicon: 1, id: 'tools.ozone.signature.defs', @@ -12764,6 +13058,13 @@ export const ids = { ToolsOzoneModerationQueryStatuses: 'tools.ozone.moderation.queryStatuses', ToolsOzoneModerationSearchRepos: 'tools.ozone.moderation.searchRepos', ToolsOzoneServerGetConfig: 'tools.ozone.server.getConfig', + ToolsOzoneSetAddValues: 'tools.ozone.set.addValues', + ToolsOzoneSetDefs: 'tools.ozone.set.defs', + ToolsOzoneSetDeleteSet: 'tools.ozone.set.deleteSet', + ToolsOzoneSetDeleteValues: 'tools.ozone.set.deleteValues', + ToolsOzoneSetGetValues: 'tools.ozone.set.getValues', + ToolsOzoneSetQuerySets: 'tools.ozone.set.querySets', + ToolsOzoneSetUpsertSet: 'tools.ozone.set.upsertSet', ToolsOzoneSignatureDefs: 'tools.ozone.signature.defs', ToolsOzoneSignatureFindCorrelation: 'tools.ozone.signature.findCorrelation', ToolsOzoneSignatureFindRelatedAccounts: diff --git a/packages/ozone/src/lexicon/types/tools/ozone/set/addValues.ts b/packages/ozone/src/lexicon/types/tools/ozone/set/addValues.ts new file mode 100644 index 00000000000..1a48da4e682 --- /dev/null +++ b/packages/ozone/src/lexicon/types/tools/ozone/set/addValues.ts @@ -0,0 +1,41 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export interface InputSchema { + /** Name of the set to add values to */ + name: string + /** Array of string values to add to the set */ + values: string[] + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerError { + status: number + message?: string +} + +export type HandlerOutput = HandlerError | void +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/ozone/src/lexicon/types/tools/ozone/set/defs.ts b/packages/ozone/src/lexicon/types/tools/ozone/set/defs.ts new file mode 100644 index 00000000000..4256bf8a90f --- /dev/null +++ b/packages/ozone/src/lexicon/types/tools/ozone/set/defs.ts @@ -0,0 +1,44 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' + +export interface Set { + name: string + description?: string + [k: string]: unknown +} + +export function isSet(v: unknown): v is Set { + return ( + isObj(v) && hasProp(v, '$type') && v.$type === 'tools.ozone.set.defs#set' + ) +} + +export function validateSet(v: unknown): ValidationResult { + return lexicons.validate('tools.ozone.set.defs#set', v) +} + +export interface SetView { + name: string + description?: string + setSize: number + createdAt: string + updatedAt: string + [k: string]: unknown +} + +export function isSetView(v: unknown): v is SetView { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'tools.ozone.set.defs#setView' + ) +} + +export function validateSetView(v: unknown): ValidationResult { + return lexicons.validate('tools.ozone.set.defs#setView', v) +} diff --git a/packages/ozone/src/lexicon/types/tools/ozone/set/deleteSet.ts b/packages/ozone/src/lexicon/types/tools/ozone/set/deleteSet.ts new file mode 100644 index 00000000000..8b1584c3a1a --- /dev/null +++ b/packages/ozone/src/lexicon/types/tools/ozone/set/deleteSet.ts @@ -0,0 +1,50 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export interface InputSchema { + /** Name of the set to delete */ + name: string + [k: string]: unknown +} + +export interface OutputSchema { + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string + error?: 'SetNotFound' +} + +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/ozone/src/lexicon/types/tools/ozone/set/deleteValues.ts b/packages/ozone/src/lexicon/types/tools/ozone/set/deleteValues.ts new file mode 100644 index 00000000000..d1090863925 --- /dev/null +++ b/packages/ozone/src/lexicon/types/tools/ozone/set/deleteValues.ts @@ -0,0 +1,42 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export interface InputSchema { + /** Name of the set to delete values from */ + name: string + /** Array of string values to delete from the set */ + values: string[] + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerError { + status: number + message?: string + error?: 'SetNotFound' +} + +export type HandlerOutput = HandlerError | void +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/ozone/src/lexicon/types/tools/ozone/set/getValues.ts b/packages/ozone/src/lexicon/types/tools/ozone/set/getValues.ts new file mode 100644 index 00000000000..abae3c0b33a --- /dev/null +++ b/packages/ozone/src/lexicon/types/tools/ozone/set/getValues.ts @@ -0,0 +1,51 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' +import * as ToolsOzoneSetDefs from './defs' + +export interface QueryParams { + name: string + limit: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + set: ToolsOzoneSetDefs.SetView + values: string[] + cursor?: string + [k: string]: unknown +} + +export type HandlerInput = undefined + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string + error?: 'SetNotFound' +} + +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/ozone/src/lexicon/types/tools/ozone/set/querySets.ts b/packages/ozone/src/lexicon/types/tools/ozone/set/querySets.ts new file mode 100644 index 00000000000..1eea93a7c4c --- /dev/null +++ b/packages/ozone/src/lexicon/types/tools/ozone/set/querySets.ts @@ -0,0 +1,52 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' +import * as ToolsOzoneSetDefs from './defs' + +export interface QueryParams { + limit: number + cursor?: string + namePrefix?: string + sortBy: 'name' | 'createdAt' | 'updatedAt' + /** Defaults to ascending order of name field. */ + sortDirection: 'asc' | 'desc' +} + +export type InputSchema = undefined + +export interface OutputSchema { + sets: ToolsOzoneSetDefs.SetView[] + cursor?: string + [k: string]: unknown +} + +export type HandlerInput = undefined + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/ozone/src/lexicon/types/tools/ozone/set/upsertSet.ts b/packages/ozone/src/lexicon/types/tools/ozone/set/upsertSet.ts new file mode 100644 index 00000000000..f51e375e425 --- /dev/null +++ b/packages/ozone/src/lexicon/types/tools/ozone/set/upsertSet.ts @@ -0,0 +1,43 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' +import * as ToolsOzoneSetDefs from './defs' + +export interface QueryParams {} + +export type InputSchema = ToolsOzoneSetDefs.Set +export type OutputSchema = ToolsOzoneSetDefs.SetView + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/ozone/src/set/service.ts b/packages/ozone/src/set/service.ts new file mode 100644 index 00000000000..6d58a4775e9 --- /dev/null +++ b/packages/ozone/src/set/service.ts @@ -0,0 +1,227 @@ +import Database from '../db' +import { Selectable } from 'kysely' +import { SetDetail } from '../db/schema/ozone_set' +import { SetView } from '../lexicon/types/tools/ozone/set/defs' +import { paginate, TimeIdKeyset } from '../db/pagination' + +export type SetServiceCreator = (db: Database) => SetService + +export class SetService { + constructor(public db: Database) {} + + static creator() { + return (db: Database) => new SetService(db) + } + + buildQueryForSetWithSize() { + return this.db.db.selectFrom('set_detail as s').select([ + 's.id', + 's.name', + 's.description', + 's.createdAt', + 's.updatedAt', + (eb) => + eb + .selectFrom('set_value') + .select((e) => e.fn.count('setId').as('count')) + .whereRef('setId', '=', 's.id') + .as('setSize'), + ]) + } + + async query({ + limit, + cursor, + namePrefix, + sortBy, + sortDirection, + }: { + limit: number + cursor?: string + namePrefix?: string + sortBy: 'name' | 'createdAt' | 'updatedAt' + sortDirection: 'asc' | 'desc' + }): Promise<{ + sets: Selectable[] + cursor?: string + }> { + let qb = this.buildQueryForSetWithSize().limit(limit) + + if (namePrefix) { + qb = qb.where('s.name', 'like', `${namePrefix}%`) + } + + if (cursor) { + if (sortBy === 'name') { + qb = qb.where('s.name', sortDirection === 'asc' ? '>' : '<', cursor) + } else { + qb = qb.where( + `s.${sortBy}`, + sortDirection === 'asc' ? '>' : '<', + new Date(cursor), + ) + } + } + + qb = qb.orderBy(`s.${sortBy}`, sortDirection) + + const sets = await qb.execute() + const lastItem = sets.at(-1) + + return { + sets, + cursor: lastItem + ? sortBy === 'name' + ? lastItem?.name + : lastItem?.[sortBy].toISOString() + : undefined, + } + } + + async getByName(name: string): Promise | undefined> { + const query = this.db.db + .selectFrom('set_detail') + .selectAll() + .where('name', '=', name) + + return await query.executeTakeFirst() + } + + async getByNameWithSize( + name: string, + ): Promise | undefined> { + return await this.buildQueryForSetWithSize() + .where('s.name', '=', name) + .executeTakeFirst() + } + + async getSetWithValues({ + name, + limit, + cursor, + }: { + name: string + limit: number + cursor?: string + }): Promise< + | { + set: Selectable + values: string[] + cursor?: string + } + | undefined + > { + const set = await this.getByNameWithSize(name) + if (!set) return undefined + + const { ref } = this.db.db.dynamic + const qb = this.db.db + .selectFrom('set_value') + .selectAll() + .where('setId', '=', set.id) + + const keyset = new TimeIdKeyset(ref(`createdAt`), ref('id')) + const paginatedBuilder = paginate(qb, { + limit, + cursor, + keyset, + direction: 'asc', + }) + + const result = await paginatedBuilder.execute() + + return { + set, + values: result.map((v) => v.value), + cursor: keyset.packFromResult(result), + } + } + async upsert({ + name, + description, + }: Pick): Promise { + await this.db.db + .insertInto('set_detail') + .values({ + name, + description, + updatedAt: new Date(), + }) + .onConflict((oc) => { + // if description is provided as a string, even an empty one, update it + // otherwise, just update the updatedAt timestamp + return oc.column('name').doUpdateSet( + typeof description === 'string' + ? { + description, + updatedAt: new Date(), + } + : { updatedAt: new Date() }, + ) + }) + .execute() + } + + async addValues(setId: number, values: string[]): Promise { + await this.db.transaction(async (txn) => { + const now = new Date() + const query = txn.db + .insertInto('set_value') + .values( + values.map((value) => ({ + setId, + value, + createdAt: now, + })), + ) + .onConflict((oc) => oc.columns(['setId', 'value']).doNothing()) + + await query.execute() + + // Update the set's updatedAt timestamp + await txn.db + .updateTable('set_detail') + .set({ updatedAt: now }) + .where('id', '=', setId) + .execute() + }) + } + + async removeValues(setId: number, values: string[]): Promise { + if (values.length < 1) { + return + } + await this.db.transaction(async (txn) => { + const query = txn.db + .deleteFrom('set_value') + .where('setId', '=', setId) + .where('value', 'in', values) + + await query.execute() + + // Update the set's updatedAt timestamp + await txn.db + .updateTable('set_detail') + .set({ updatedAt: new Date() }) + .where('id', '=', setId) + .execute() + }) + } + + async removeSet(setId: number): Promise { + await this.db.transaction(async (txn) => { + await txn.db.deleteFrom('set_value').where('setId', '=', setId).execute() + await txn.db.deleteFrom('set_detail').where('id', '=', setId).execute() + }) + } + + view(set: Selectable & { setSize: number }): SetView { + return { + name: set.name, + description: set.description || undefined, + setSize: set.setSize, + createdAt: set.createdAt.toISOString(), + updatedAt: set.updatedAt.toISOString(), + } + } +} diff --git a/packages/ozone/tests/__snapshots__/sets.test.ts.snap b/packages/ozone/tests/__snapshots__/sets.test.ts.snap new file mode 100644 index 00000000000..15a8a4cf592 --- /dev/null +++ b/packages/ozone/tests/__snapshots__/sets.test.ts.snap @@ -0,0 +1,46 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ozone-sets querySets returns all sets when no parameters are provided 1`] = ` +Array [ + Object { + "createdAt": "1970-01-01T00:00:00.000Z", + "description": "Another test set", + "name": "another-set", + "setSize": 0, + "updatedAt": "1970-01-01T00:00:00.000Z", + }, + Object { + "createdAt": "1970-01-01T00:00:00.000Z", + "description": "Test set 1", + "name": "test-set-1", + "setSize": 0, + "updatedAt": "1970-01-01T00:00:00.000Z", + }, + Object { + "createdAt": "1970-01-01T00:00:00.000Z", + "name": "test-set-2", + "setSize": 0, + "updatedAt": "1970-01-01T00:00:00.000Z", + }, +] +`; + +exports[`ozone-sets upsertSet creates a new set 1`] = ` +Object { + "createdAt": "1970-01-01T00:00:00.000Z", + "description": "A new test set", + "name": "new-test-set", + "setSize": 0, + "updatedAt": "1970-01-01T00:00:00.000Z", +} +`; + +exports[`ozone-sets upsertSet updates an existing set 1`] = ` +Object { + "createdAt": "1970-01-01T00:00:00.000Z", + "description": "Updated description", + "name": "new-test-set", + "setSize": 0, + "updatedAt": "1970-01-01T00:00:00.000Z", +} +`; diff --git a/packages/ozone/tests/sets.test.ts b/packages/ozone/tests/sets.test.ts new file mode 100644 index 00000000000..90bf810357b --- /dev/null +++ b/packages/ozone/tests/sets.test.ts @@ -0,0 +1,246 @@ +import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env' +import AtpAgent, { + ToolsOzoneSetDefs, + ToolsOzoneSetQuerySets, +} from '@atproto/api' +import { forSnapshot } from './_util' +import { ids } from '../src/lexicon/lexicons' + +describe('ozone-sets', () => { + let network: TestNetwork + let agent: AtpAgent + let sc: SeedClient + + const sampleSet1 = { + name: 'test-set-1', + description: 'Test set 1', + } + + const sampleSet2 = { + name: 'test-set-2', + } + + const sampleSet3 = { + name: 'another-set', + description: 'Another test set', + } + + const upsertSet = async (set: ToolsOzoneSetDefs.Set) => { + const { data } = await agent.tools.ozone.set.upsertSet(set, { + encoding: 'application/json', + headers: await network.ozone.modHeaders( + ids.ToolsOzoneSetUpsertSet, + 'admin', + ), + }) + + return data + } + + const removeSet = async (name: string) => { + await agent.tools.ozone.set.deleteSet( + { name }, + { + encoding: 'application/json', + headers: await network.ozone.modHeaders( + ids.ToolsOzoneSetDeleteSet, + 'admin', + ), + }, + ) + } + + const addValues = async (name: string, values: string[]) => { + await agent.tools.ozone.set.addValues( + { name, values }, + { + encoding: 'application/json', + headers: await network.ozone.modHeaders( + ids.ToolsOzoneSetAddValues, + 'admin', + ), + }, + ) + } + + const getValues = async (name: string, limit?: number, cursor?: string) => { + const { data } = await agent.tools.ozone.set.getValues( + { name, limit, cursor }, + { + headers: await network.ozone.modHeaders( + ids.ToolsOzoneSetGetValues, + 'moderator', + ), + }, + ) + return data + } + + const querySets = async (params: ToolsOzoneSetQuerySets.QueryParams) => { + const { data } = await agent.tools.ozone.set.querySets(params, { + headers: await network.ozone.modHeaders( + ids.ToolsOzoneSetQuerySets, + 'moderator', + ), + }) + return data + } + + beforeAll(async () => { + network = await TestNetwork.create({ + dbPostgresSchema: 'ozone_sets', + }) + agent = network.ozone.getClient() + sc = network.getSeedClient() + await basicSeed(sc) + await network.processAll() + }) + + afterAll(async () => { + await network.close() + }) + + describe('querySets', () => { + beforeAll(async () => { + await Promise.all([ + upsertSet(sampleSet1), + upsertSet(sampleSet2), + upsertSet(sampleSet3), + ]) + }) + afterAll(async () => { + await Promise.all([ + removeSet(sampleSet1.name), + removeSet(sampleSet2.name), + removeSet(sampleSet3.name), + ]) + }) + it('returns all sets when no parameters are provided', async () => { + const result = await querySets({}) + expect(result.sets.length).toBe(3) + expect(forSnapshot(result.sets)).toMatchSnapshot() + }) + + it('limits the number of returned sets', async () => { + const result = await querySets({ limit: 2 }) + expect(result.sets.length).toBe(2) + expect(result.cursor).toBeDefined() + }) + + it('returns sets after the cursor', async () => { + const firstPage = await querySets({ limit: 2 }) + const secondPage = await querySets({ cursor: firstPage.cursor }) + expect(secondPage.sets.length).toBe(1) + expect(secondPage.sets[0].name).toBe('test-set-2') + }) + + it('filters sets by name prefix', async () => { + const result = await querySets({ namePrefix: 'test-' }) + expect(result.sets.length).toBe(2) + expect(result.sets.map((s) => s.name)).toEqual([ + 'test-set-1', + 'test-set-2', + ]) + }) + + it('sorts sets by given column and direction', async () => { + const sortedByName = await querySets({ sortBy: 'name' }) + expect(sortedByName.sets.map((s) => s.name)).toEqual([ + 'another-set', + 'test-set-1', + 'test-set-2', + ]) + const reverseSortedByName = await querySets({ + sortBy: 'name', + sortDirection: 'desc', + }) + expect(reverseSortedByName.sets.map((s) => s.name)).toEqual([ + 'test-set-2', + 'test-set-1', + 'another-set', + ]) + }) + }) + + describe('upsertSet', () => { + afterAll(async () => { + await removeSet('new-test-set') + }) + it('creates a new set', async () => { + const result = await upsertSet({ + name: 'new-test-set', + description: 'A new test set', + }) + expect(forSnapshot(result)).toMatchSnapshot() + }) + + it('updates an existing set', async () => { + const result = await upsertSet({ + name: 'new-test-set', + description: 'Updated description', + }) + expect(forSnapshot(result)).toMatchSnapshot() + }) + + it('allows setting empty description', async () => { + const result = await upsertSet({ + name: 'new-test-set', + description: '', + }) + expect(result.description).toBeUndefined() + }) + }) + + describe('addValues', () => { + beforeAll(async () => { + await upsertSet(sampleSet1) + await upsertSet(sampleSet2) + }) + afterAll(async () => { + await removeSet(sampleSet1.name) + await removeSet(sampleSet2.name) + }) + it('adds new values to an existing set', async () => { + const newValues = ['value1', 'value2', 'value3'] + await addValues(sampleSet1.name, newValues) + + const result = await getValues(sampleSet1.name) + expect(result.values).toEqual(expect.arrayContaining(newValues)) + }) + + it('does not duplicate existing values', async () => { + const initialValues = ['initial1', 'initial2'] + await addValues(sampleSet2.name, initialValues) + + const newValues = ['initial2', 'new1', 'new2'] + await addValues(sampleSet2.name, newValues) + + const result = await getValues(sampleSet2.name) + expect(result.values).toEqual( + expect.arrayContaining([...initialValues, 'new1', 'new2']), + ) + expect(result.values.filter((v) => v === 'initial2').length).toBe(1) + }) + }) + + describe('getValues', () => { + beforeAll(async () => { + await upsertSet(sampleSet1) + }) + afterAll(async () => { + await removeSet(sampleSet1.name) + }) + it('paginates values from a set', async () => { + const allValues = Array.from({ length: 9 }, (_, i) => `value${i}`) + await addValues(sampleSet1.name, allValues) + + const firstPage = await getValues(sampleSet1.name, 3) + const secondPage = await getValues(sampleSet1.name, 3, firstPage.cursor) + const lastPage = await getValues(sampleSet1.name, 3, secondPage.cursor) + + expect(firstPage.values).toEqual(allValues.slice(0, 3)) + expect(secondPage.values).toEqual(allValues.slice(3, 6)) + expect(lastPage.values).toEqual(allValues.slice(6, 9)) + }) + }) +}) diff --git a/packages/pds/src/lexicon/index.ts b/packages/pds/src/lexicon/index.ts index 97c1a3b9f35..d2edd584b1a 100644 --- a/packages/pds/src/lexicon/index.ts +++ b/packages/pds/src/lexicon/index.ts @@ -173,6 +173,12 @@ import * as ToolsOzoneModerationQueryEvents from './types/tools/ozone/moderation import * as ToolsOzoneModerationQueryStatuses from './types/tools/ozone/moderation/queryStatuses' import * as ToolsOzoneModerationSearchRepos from './types/tools/ozone/moderation/searchRepos' import * as ToolsOzoneServerGetConfig from './types/tools/ozone/server/getConfig' +import * as ToolsOzoneSetAddValues from './types/tools/ozone/set/addValues' +import * as ToolsOzoneSetDeleteSet from './types/tools/ozone/set/deleteSet' +import * as ToolsOzoneSetDeleteValues from './types/tools/ozone/set/deleteValues' +import * as ToolsOzoneSetGetValues from './types/tools/ozone/set/getValues' +import * as ToolsOzoneSetQuerySets from './types/tools/ozone/set/querySets' +import * as ToolsOzoneSetUpsertSet from './types/tools/ozone/set/upsertSet' import * as ToolsOzoneSignatureFindCorrelation from './types/tools/ozone/signature/findCorrelation' import * as ToolsOzoneSignatureFindRelatedAccounts from './types/tools/ozone/signature/findRelatedAccounts' import * as ToolsOzoneSignatureSearchAccounts from './types/tools/ozone/signature/searchAccounts' @@ -2164,6 +2170,7 @@ export class ToolsOzoneNS { communication: ToolsOzoneCommunicationNS moderation: ToolsOzoneModerationNS server: ToolsOzoneServerNS + set: ToolsOzoneSetNS signature: ToolsOzoneSignatureNS team: ToolsOzoneTeamNS @@ -2172,6 +2179,7 @@ export class ToolsOzoneNS { this.communication = new ToolsOzoneCommunicationNS(server) this.moderation = new ToolsOzoneModerationNS(server) this.server = new ToolsOzoneServerNS(server) + this.set = new ToolsOzoneSetNS(server) this.signature = new ToolsOzoneSignatureNS(server) this.team = new ToolsOzoneTeamNS(server) } @@ -2355,6 +2363,80 @@ export class ToolsOzoneServerNS { } } +export class ToolsOzoneSetNS { + _server: Server + + constructor(server: Server) { + this._server = server + } + + addValues( + cfg: ConfigOf< + AV, + ToolsOzoneSetAddValues.Handler>, + ToolsOzoneSetAddValues.HandlerReqCtx> + >, + ) { + const nsid = 'tools.ozone.set.addValues' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + deleteSet( + cfg: ConfigOf< + AV, + ToolsOzoneSetDeleteSet.Handler>, + ToolsOzoneSetDeleteSet.HandlerReqCtx> + >, + ) { + const nsid = 'tools.ozone.set.deleteSet' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + deleteValues( + cfg: ConfigOf< + AV, + ToolsOzoneSetDeleteValues.Handler>, + ToolsOzoneSetDeleteValues.HandlerReqCtx> + >, + ) { + const nsid = 'tools.ozone.set.deleteValues' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getValues( + cfg: ConfigOf< + AV, + ToolsOzoneSetGetValues.Handler>, + ToolsOzoneSetGetValues.HandlerReqCtx> + >, + ) { + const nsid = 'tools.ozone.set.getValues' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + querySets( + cfg: ConfigOf< + AV, + ToolsOzoneSetQuerySets.Handler>, + ToolsOzoneSetQuerySets.HandlerReqCtx> + >, + ) { + const nsid = 'tools.ozone.set.querySets' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + upsertSet( + cfg: ConfigOf< + AV, + ToolsOzoneSetUpsertSet.Handler>, + ToolsOzoneSetUpsertSet.HandlerReqCtx> + >, + ) { + const nsid = 'tools.ozone.set.upsertSet' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } +} + export class ToolsOzoneSignatureNS { _server: Server diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 7b373bca624..2b9f298ad96 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -12139,6 +12139,300 @@ export const schemaDict = { }, }, }, + ToolsOzoneSetAddValues: { + lexicon: 1, + id: 'tools.ozone.set.addValues', + defs: { + main: { + type: 'procedure', + description: + 'Add values to a specific set. Attempting to add values to a set that does not exist will result in an error.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['name', 'values'], + properties: { + name: { + type: 'string', + description: 'Name of the set to add values to', + }, + values: { + type: 'array', + minLength: 1, + maxLength: 1000, + items: { + type: 'string', + }, + description: 'Array of string values to add to the set', + }, + }, + }, + }, + }, + }, + }, + ToolsOzoneSetDefs: { + lexicon: 1, + id: 'tools.ozone.set.defs', + defs: { + set: { + type: 'object', + required: ['name'], + properties: { + name: { + type: 'string', + minLength: 3, + maxLength: 128, + }, + description: { + type: 'string', + maxGraphemes: 1024, + maxLength: 10240, + }, + }, + }, + setView: { + type: 'object', + required: ['name', 'setSize', 'createdAt', 'updatedAt'], + properties: { + name: { + type: 'string', + minLength: 3, + maxLength: 128, + }, + description: { + type: 'string', + maxGraphemes: 1024, + maxLength: 10240, + }, + setSize: { + type: 'integer', + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + updatedAt: { + type: 'string', + format: 'datetime', + }, + }, + }, + }, + }, + ToolsOzoneSetDeleteSet: { + lexicon: 1, + id: 'tools.ozone.set.deleteSet', + defs: { + main: { + type: 'procedure', + description: + 'Delete an entire set. Attempting to delete a set that does not exist will result in an error.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['name'], + properties: { + name: { + type: 'string', + description: 'Name of the set to delete', + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + properties: {}, + }, + }, + errors: [ + { + name: 'SetNotFound', + description: 'set with the given name does not exist', + }, + ], + }, + }, + }, + ToolsOzoneSetDeleteValues: { + lexicon: 1, + id: 'tools.ozone.set.deleteValues', + defs: { + main: { + type: 'procedure', + description: + 'Delete values from a specific set. Attempting to delete values that are not in the set will not result in an error', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['name', 'values'], + properties: { + name: { + type: 'string', + description: 'Name of the set to delete values from', + }, + values: { + type: 'array', + minLength: 1, + items: { + type: 'string', + }, + description: 'Array of string values to delete from the set', + }, + }, + }, + }, + errors: [ + { + name: 'SetNotFound', + description: 'set with the given name does not exist', + }, + ], + }, + }, + }, + ToolsOzoneSetGetValues: { + lexicon: 1, + id: 'tools.ozone.set.getValues', + defs: { + main: { + type: 'query', + description: 'Get a specific set and its values', + parameters: { + type: 'params', + required: ['name'], + properties: { + name: { + type: 'string', + }, + limit: { + type: 'integer', + minimum: 1, + maximum: 1000, + default: 100, + }, + cursor: { + type: 'string', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['set', 'values'], + properties: { + set: { + type: 'ref', + ref: 'lex:tools.ozone.set.defs#setView', + }, + values: { + type: 'array', + items: { + type: 'string', + }, + }, + cursor: { + type: 'string', + }, + }, + }, + }, + errors: [ + { + name: 'SetNotFound', + description: 'set with the given name does not exist', + }, + ], + }, + }, + }, + ToolsOzoneSetQuerySets: { + lexicon: 1, + id: 'tools.ozone.set.querySets', + defs: { + main: { + type: 'query', + description: 'Query available sets', + parameters: { + type: 'params', + properties: { + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 50, + }, + cursor: { + type: 'string', + }, + namePrefix: { + type: 'string', + }, + sortBy: { + type: 'string', + enum: ['name', 'createdAt', 'updatedAt'], + default: 'name', + }, + sortDirection: { + type: 'string', + default: 'asc', + enum: ['asc', 'desc'], + description: 'Defaults to ascending order of name field.', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['sets'], + properties: { + sets: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:tools.ozone.set.defs#setView', + }, + }, + cursor: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + ToolsOzoneSetUpsertSet: { + lexicon: 1, + id: 'tools.ozone.set.upsertSet', + defs: { + main: { + type: 'procedure', + description: 'Create or update set metadata', + input: { + encoding: 'application/json', + schema: { + type: 'ref', + ref: 'lex:tools.ozone.set.defs#set', + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'ref', + ref: 'lex:tools.ozone.set.defs#setView', + }, + }, + }, + }, + }, ToolsOzoneSignatureDefs: { lexicon: 1, id: 'tools.ozone.signature.defs', @@ -12764,6 +13058,13 @@ export const ids = { ToolsOzoneModerationQueryStatuses: 'tools.ozone.moderation.queryStatuses', ToolsOzoneModerationSearchRepos: 'tools.ozone.moderation.searchRepos', ToolsOzoneServerGetConfig: 'tools.ozone.server.getConfig', + ToolsOzoneSetAddValues: 'tools.ozone.set.addValues', + ToolsOzoneSetDefs: 'tools.ozone.set.defs', + ToolsOzoneSetDeleteSet: 'tools.ozone.set.deleteSet', + ToolsOzoneSetDeleteValues: 'tools.ozone.set.deleteValues', + ToolsOzoneSetGetValues: 'tools.ozone.set.getValues', + ToolsOzoneSetQuerySets: 'tools.ozone.set.querySets', + ToolsOzoneSetUpsertSet: 'tools.ozone.set.upsertSet', ToolsOzoneSignatureDefs: 'tools.ozone.signature.defs', ToolsOzoneSignatureFindCorrelation: 'tools.ozone.signature.findCorrelation', ToolsOzoneSignatureFindRelatedAccounts: diff --git a/packages/pds/src/lexicon/types/tools/ozone/set/addValues.ts b/packages/pds/src/lexicon/types/tools/ozone/set/addValues.ts new file mode 100644 index 00000000000..1a48da4e682 --- /dev/null +++ b/packages/pds/src/lexicon/types/tools/ozone/set/addValues.ts @@ -0,0 +1,41 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export interface InputSchema { + /** Name of the set to add values to */ + name: string + /** Array of string values to add to the set */ + values: string[] + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerError { + status: number + message?: string +} + +export type HandlerOutput = HandlerError | void +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/tools/ozone/set/defs.ts b/packages/pds/src/lexicon/types/tools/ozone/set/defs.ts new file mode 100644 index 00000000000..4256bf8a90f --- /dev/null +++ b/packages/pds/src/lexicon/types/tools/ozone/set/defs.ts @@ -0,0 +1,44 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' + +export interface Set { + name: string + description?: string + [k: string]: unknown +} + +export function isSet(v: unknown): v is Set { + return ( + isObj(v) && hasProp(v, '$type') && v.$type === 'tools.ozone.set.defs#set' + ) +} + +export function validateSet(v: unknown): ValidationResult { + return lexicons.validate('tools.ozone.set.defs#set', v) +} + +export interface SetView { + name: string + description?: string + setSize: number + createdAt: string + updatedAt: string + [k: string]: unknown +} + +export function isSetView(v: unknown): v is SetView { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'tools.ozone.set.defs#setView' + ) +} + +export function validateSetView(v: unknown): ValidationResult { + return lexicons.validate('tools.ozone.set.defs#setView', v) +} diff --git a/packages/pds/src/lexicon/types/tools/ozone/set/deleteSet.ts b/packages/pds/src/lexicon/types/tools/ozone/set/deleteSet.ts new file mode 100644 index 00000000000..8b1584c3a1a --- /dev/null +++ b/packages/pds/src/lexicon/types/tools/ozone/set/deleteSet.ts @@ -0,0 +1,50 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export interface InputSchema { + /** Name of the set to delete */ + name: string + [k: string]: unknown +} + +export interface OutputSchema { + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string + error?: 'SetNotFound' +} + +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/tools/ozone/set/deleteValues.ts b/packages/pds/src/lexicon/types/tools/ozone/set/deleteValues.ts new file mode 100644 index 00000000000..d1090863925 --- /dev/null +++ b/packages/pds/src/lexicon/types/tools/ozone/set/deleteValues.ts @@ -0,0 +1,42 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export interface InputSchema { + /** Name of the set to delete values from */ + name: string + /** Array of string values to delete from the set */ + values: string[] + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerError { + status: number + message?: string + error?: 'SetNotFound' +} + +export type HandlerOutput = HandlerError | void +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/tools/ozone/set/getValues.ts b/packages/pds/src/lexicon/types/tools/ozone/set/getValues.ts new file mode 100644 index 00000000000..abae3c0b33a --- /dev/null +++ b/packages/pds/src/lexicon/types/tools/ozone/set/getValues.ts @@ -0,0 +1,51 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' +import * as ToolsOzoneSetDefs from './defs' + +export interface QueryParams { + name: string + limit: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + set: ToolsOzoneSetDefs.SetView + values: string[] + cursor?: string + [k: string]: unknown +} + +export type HandlerInput = undefined + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string + error?: 'SetNotFound' +} + +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/tools/ozone/set/querySets.ts b/packages/pds/src/lexicon/types/tools/ozone/set/querySets.ts new file mode 100644 index 00000000000..1eea93a7c4c --- /dev/null +++ b/packages/pds/src/lexicon/types/tools/ozone/set/querySets.ts @@ -0,0 +1,52 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' +import * as ToolsOzoneSetDefs from './defs' + +export interface QueryParams { + limit: number + cursor?: string + namePrefix?: string + sortBy: 'name' | 'createdAt' | 'updatedAt' + /** Defaults to ascending order of name field. */ + sortDirection: 'asc' | 'desc' +} + +export type InputSchema = undefined + +export interface OutputSchema { + sets: ToolsOzoneSetDefs.SetView[] + cursor?: string + [k: string]: unknown +} + +export type HandlerInput = undefined + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/tools/ozone/set/upsertSet.ts b/packages/pds/src/lexicon/types/tools/ozone/set/upsertSet.ts new file mode 100644 index 00000000000..f51e375e425 --- /dev/null +++ b/packages/pds/src/lexicon/types/tools/ozone/set/upsertSet.ts @@ -0,0 +1,43 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' +import * as ToolsOzoneSetDefs from './defs' + +export interface QueryParams {} + +export type InputSchema = ToolsOzoneSetDefs.Set +export type OutputSchema = ToolsOzoneSetDefs.SetView + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput