@atproto/bsync 0.0.23 → 0.0.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const connect_1 = require("@connectrpc/connect");
4
+ const bsync_pb_1 = require("../proto/bsync_pb");
5
+ const auth_1 = require("./auth");
6
+ const util_1 = require("./util");
7
+ exports.default = (ctx) => ({
8
+ /**
9
+ * This method is responsible for deleting log rows from the bsync db, it has
10
+ * no other downstream effects. This method is called from the dataplane in
11
+ * response to a data deletion request initiated by a moderator in Ozone.
12
+ * It's the final step of the deletion process, basically cleaning up the
13
+ * breadcrumbs that resulted in the state we store in the dataplane.
14
+ */
15
+ async deleteOperationsByActorAndNamespace(req, handlerCtx) {
16
+ (0, auth_1.authWithApiKey)(ctx, handlerCtx);
17
+ const { db } = ctx;
18
+ try {
19
+ (0, util_1.validateNamespace)(req.namespace);
20
+ }
21
+ catch (error) {
22
+ throw new connect_1.ConnectError('requested namespace for deletion is invalid NSID', connect_1.Code.InvalidArgument);
23
+ }
24
+ if (!(0, util_1.isValidDid)(req.actorDid)) {
25
+ throw new connect_1.ConnectError('requested actor_did for deletion is invalid DID', connect_1.Code.InvalidArgument);
26
+ }
27
+ const deletedRows = await db.db
28
+ .deleteFrom('operation')
29
+ .where('actorDid', '=', req.actorDid)
30
+ .where('namespace', '=', req.namespace)
31
+ .returning('id')
32
+ .execute();
33
+ return new bsync_pb_1.DeleteOperationsByActorAndNamespaceResponse({
34
+ deletedCount: deletedRows.length,
35
+ });
36
+ },
37
+ });
38
+ //# sourceMappingURL=delete-operations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delete-operations.js","sourceRoot":"","sources":["../../src/routes/delete-operations.ts"],"names":[],"mappings":";;AAAA,iDAAqE;AAGrE,gDAA+E;AAC/E,iCAAuC;AACvC,iCAAsD;AAEtD,kBAAe,CAAC,GAAe,EAAwC,EAAE,CAAC,CAAC;IACzE;;;;;;OAMG;IACH,KAAK,CAAC,mCAAmC,CAAC,GAAG,EAAE,UAAU;QACvD,IAAA,qBAAc,EAAC,GAAG,EAAE,UAAU,CAAC,CAAA;QAC/B,MAAM,EAAE,EAAE,EAAE,GAAG,GAAG,CAAA;QAElB,IAAI,CAAC;YACH,IAAA,wBAAiB,EAAC,GAAG,CAAC,SAAS,CAAC,CAAA;QAClC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,sBAAY,CACpB,kDAAkD,EAClD,cAAI,CAAC,eAAe,CACrB,CAAA;QACH,CAAC;QACD,IAAI,CAAC,IAAA,iBAAU,EAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC9B,MAAM,IAAI,sBAAY,CACpB,iDAAiD,EACjD,cAAI,CAAC,eAAe,CACrB,CAAA;QACH,CAAC;QAED,MAAM,WAAW,GAAG,MAAM,EAAE,CAAC,EAAE;aAC5B,UAAU,CAAC,WAAW,CAAC;aACvB,KAAK,CAAC,UAAU,EAAE,GAAG,EAAE,GAAG,CAAC,QAAQ,CAAC;aACpC,KAAK,CAAC,WAAW,EAAE,GAAG,EAAE,GAAG,CAAC,SAAS,CAAC;aACtC,SAAS,CAAC,IAAI,CAAC;aACf,OAAO,EAAE,CAAA;QACZ,OAAO,IAAI,sDAA2C,CAAC;YACrD,YAAY,EAAE,WAAW,CAAC,MAAM;SACjC,CAAC,CAAA;IACJ,CAAC;CACF,CAAC,CAAA","sourcesContent":["import { Code, ConnectError, ServiceImpl } from '@connectrpc/connect'\nimport { AppContext } from '../context'\nimport { Service } from '../proto/bsync_connect'\nimport { DeleteOperationsByActorAndNamespaceResponse } from '../proto/bsync_pb'\nimport { authWithApiKey } from './auth'\nimport { isValidDid, validateNamespace } from './util'\n\nexport default (ctx: AppContext): Partial<ServiceImpl<typeof Service>> => ({\n /**\n * This method is responsible for deleting log rows from the bsync db, it has\n * no other downstream effects. This method is called from the dataplane in\n * response to a data deletion request initiated by a moderator in Ozone.\n * It's the final step of the deletion process, basically cleaning up the\n * breadcrumbs that resulted in the state we store in the dataplane.\n */\n async deleteOperationsByActorAndNamespace(req, handlerCtx) {\n authWithApiKey(ctx, handlerCtx)\n const { db } = ctx\n\n try {\n validateNamespace(req.namespace)\n } catch (error) {\n throw new ConnectError(\n 'requested namespace for deletion is invalid NSID',\n Code.InvalidArgument,\n )\n }\n if (!isValidDid(req.actorDid)) {\n throw new ConnectError(\n 'requested actor_did for deletion is invalid DID',\n Code.InvalidArgument,\n )\n }\n\n const deletedRows = await db.db\n .deleteFrom('operation')\n .where('actorDid', '=', req.actorDid)\n .where('namespace', '=', req.namespace)\n .returning('id')\n .execute()\n return new DeleteOperationsByActorAndNamespaceResponse({\n deletedCount: deletedRows.length,\n })\n },\n})\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/routes/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAEnD,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;yBASvB,KAAK,UAAU,MAAM,QAAQ,aAAa;AAA1D,wBAcC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/routes/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAEnD,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;yBAUvB,KAAK,UAAU,MAAM,QAAQ,aAAa;AAA1D,wBAeC"}
@@ -7,6 +7,7 @@ const kysely_1 = require("kysely");
7
7
  const bsync_connect_1 = require("../proto/bsync_connect");
8
8
  const add_mute_operation_1 = __importDefault(require("./add-mute-operation"));
9
9
  const add_notif_operation_1 = __importDefault(require("./add-notif-operation"));
10
+ const delete_operations_1 = __importDefault(require("./delete-operations"));
10
11
  const put_operation_1 = __importDefault(require("./put-operation"));
11
12
  const scan_mute_operations_1 = __importDefault(require("./scan-mute-operations"));
12
13
  const scan_notif_operations_1 = __importDefault(require("./scan-notif-operations"));
@@ -19,6 +20,7 @@ exports.default = (ctx) => (router) => {
19
20
  ...(0, scan_notif_operations_1.default)(ctx),
20
21
  ...(0, put_operation_1.default)(ctx),
21
22
  ...(0, scan_operations_1.default)(ctx),
23
+ ...(0, delete_operations_1.default)(ctx),
22
24
  async ping() {
23
25
  const { db } = ctx;
24
26
  await (0, kysely_1.sql) `select 1`.execute(db.db);
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/routes/index.ts"],"names":[],"mappings":";;;;;AACA,mCAA4B;AAE5B,0DAAgD;AAChD,8EAAmD;AACnD,gFAAqD;AACrD,oEAA0C;AAC1C,kFAAuD;AACvD,oFAAyD;AACzD,wEAA8C;AAE9C,kBAAe,CAAC,GAAe,EAAE,EAAE,CAAC,CAAC,MAAqB,EAAE,EAAE;IAC5D,OAAO,MAAM,CAAC,OAAO,CAAC,uBAAO,EAAE;QAC7B,GAAG,IAAA,4BAAgB,EAAC,GAAG,CAAC;QACxB,GAAG,IAAA,8BAAkB,EAAC,GAAG,CAAC;QAC1B,GAAG,IAAA,6BAAiB,EAAC,GAAG,CAAC;QACzB,GAAG,IAAA,+BAAmB,EAAC,GAAG,CAAC;QAC3B,GAAG,IAAA,uBAAY,EAAC,GAAG,CAAC;QACpB,GAAG,IAAA,yBAAc,EAAC,GAAG,CAAC;QACtB,KAAK,CAAC,IAAI;YACR,MAAM,EAAE,EAAE,EAAE,GAAG,GAAG,CAAA;YAClB,MAAM,IAAA,YAAG,EAAA,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC,CAAA;YAClC,OAAO,EAAE,CAAA;QACX,CAAC;KACF,CAAC,CAAA;AACJ,CAAC,CAAA","sourcesContent":["import { ConnectRouter } from '@connectrpc/connect'\nimport { sql } from 'kysely'\nimport { AppContext } from '../context'\nimport { Service } from '../proto/bsync_connect'\nimport addMuteOperation from './add-mute-operation'\nimport addNotifOperation from './add-notif-operation'\nimport putOperation from './put-operation'\nimport scanMuteOperations from './scan-mute-operations'\nimport scanNotifOperations from './scan-notif-operations'\nimport scanOperations from './scan-operations'\n\nexport default (ctx: AppContext) => (router: ConnectRouter) => {\n return router.service(Service, {\n ...addMuteOperation(ctx),\n ...scanMuteOperations(ctx),\n ...addNotifOperation(ctx),\n ...scanNotifOperations(ctx),\n ...putOperation(ctx),\n ...scanOperations(ctx),\n async ping() {\n const { db } = ctx\n await sql`select 1`.execute(db.db)\n return {}\n },\n })\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/routes/index.ts"],"names":[],"mappings":";;;;;AACA,mCAA4B;AAE5B,0DAAgD;AAChD,8EAAmD;AACnD,gFAAqD;AACrD,4EAAkD;AAClD,oEAA0C;AAC1C,kFAAuD;AACvD,oFAAyD;AACzD,wEAA8C;AAE9C,kBAAe,CAAC,GAAe,EAAE,EAAE,CAAC,CAAC,MAAqB,EAAE,EAAE;IAC5D,OAAO,MAAM,CAAC,OAAO,CAAC,uBAAO,EAAE;QAC7B,GAAG,IAAA,4BAAgB,EAAC,GAAG,CAAC;QACxB,GAAG,IAAA,8BAAkB,EAAC,GAAG,CAAC;QAC1B,GAAG,IAAA,6BAAiB,EAAC,GAAG,CAAC;QACzB,GAAG,IAAA,+BAAmB,EAAC,GAAG,CAAC;QAC3B,GAAG,IAAA,uBAAY,EAAC,GAAG,CAAC;QACpB,GAAG,IAAA,yBAAc,EAAC,GAAG,CAAC;QACtB,GAAG,IAAA,2BAAgB,EAAC,GAAG,CAAC;QACxB,KAAK,CAAC,IAAI;YACR,MAAM,EAAE,EAAE,EAAE,GAAG,GAAG,CAAA;YAClB,MAAM,IAAA,YAAG,EAAA,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC,CAAA;YAClC,OAAO,EAAE,CAAA;QACX,CAAC;KACF,CAAC,CAAA;AACJ,CAAC,CAAA","sourcesContent":["import { ConnectRouter } from '@connectrpc/connect'\nimport { sql } from 'kysely'\nimport { AppContext } from '../context'\nimport { Service } from '../proto/bsync_connect'\nimport addMuteOperation from './add-mute-operation'\nimport addNotifOperation from './add-notif-operation'\nimport deleteOperations from './delete-operations'\nimport putOperation from './put-operation'\nimport scanMuteOperations from './scan-mute-operations'\nimport scanNotifOperations from './scan-notif-operations'\nimport scanOperations from './scan-operations'\n\nexport default (ctx: AppContext) => (router: ConnectRouter) => {\n return router.service(Service, {\n ...addMuteOperation(ctx),\n ...scanMuteOperations(ctx),\n ...addNotifOperation(ctx),\n ...scanNotifOperations(ctx),\n ...putOperation(ctx),\n ...scanOperations(ctx),\n ...deleteOperations(ctx),\n async ping() {\n const { db } = ctx\n await sql`select 1`.execute(db.db)\n return {}\n },\n })\n}\n"]}
@@ -45,7 +45,7 @@ const putOp = async (db, op) => {
45
45
  };
46
46
  const validateOp = (req) => {
47
47
  try {
48
- validateNamespace(req.namespace);
48
+ (0, util_1.validateNamespace)(req.namespace);
49
49
  }
50
50
  catch (error) {
51
51
  throw new connect_1.ConnectError('operation namespace is invalid NSID', connect_1.Code.InvalidArgument);
@@ -77,15 +77,4 @@ const validateOp = (req) => {
77
77
  }
78
78
  return req;
79
79
  };
80
- const validateNamespace = (namespace) => {
81
- const parts = namespace.split('#');
82
- if (parts.length !== 1 && parts.length !== 2) {
83
- throw new Error('namespace must be in the format "nsid[#fragment]"');
84
- }
85
- const [nsid, fragment] = parts;
86
- (0, syntax_1.ensureValidNsid)(nsid);
87
- if (fragment && !/^[a-zA-Z][a-zA-Z0-9]*$/.test(fragment)) {
88
- throw new Error('namespace fragment must be a valid identifier');
89
- }
90
- };
91
80
  //# sourceMappingURL=put-operation.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"put-operation.js","sourceRoot":"","sources":["../../src/routes/put-operation.ts"],"names":[],"mappings":"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","sourcesContent":["import { Code, ConnectError, ServiceImpl } from '@connectrpc/connect'\nimport { sql } from 'kysely'\nimport { ensureValidNsid, ensureValidRecordKey } from '@atproto/syntax'\nimport { AppContext } from '../context'\nimport { Database } from '../db'\nimport { OperationMethod, createOperationChannel } from '../db/schema/operation'\nimport { Service } from '../proto/bsync_connect'\nimport {\n Method,\n PutOperationRequest,\n PutOperationResponse,\n} from '../proto/bsync_pb'\nimport { authWithApiKey } from './auth'\nimport { isValidDid } from './util'\n\nexport default (ctx: AppContext): Partial<ServiceImpl<typeof Service>> => ({\n async putOperation(req, handlerCtx) {\n authWithApiKey(ctx, handlerCtx)\n const { db } = ctx\n const op = validateOp(req)\n const id = await db.transaction(async (txn) => {\n return putOp(txn, op)\n })\n return new PutOperationResponse({\n operation: {\n id: String(id),\n actorDid: op.actorDid,\n namespace: op.namespace,\n key: op.key,\n method: op.method,\n payload: op.payload,\n },\n })\n },\n})\n\nconst putOp = async (db: Database, op: Operation) => {\n const { ref } = db.db.dynamic\n const { id } = await db.db\n .insertInto('operation')\n .values({\n actorDid: op.actorDid,\n namespace: op.namespace,\n key: op.key,\n method: op.method,\n payload: op.payload,\n })\n .returning('id')\n .executeTakeFirstOrThrow()\n await sql`notify ${ref(createOperationChannel)}`.execute(db.db) // emitted transactionally\n return id\n}\n\nconst validateOp = (req: PutOperationRequest): Operation => {\n try {\n validateNamespace(req.namespace)\n } catch (error) {\n throw new ConnectError(\n 'operation namespace is invalid NSID',\n Code.InvalidArgument,\n )\n }\n\n if (!isValidDid(req.actorDid)) {\n throw new ConnectError(\n 'operation actor_did is invalid DID',\n Code.InvalidArgument,\n )\n }\n\n try {\n ensureValidRecordKey(req.key)\n } catch (error) {\n throw new ConnectError('operation key is required', Code.InvalidArgument)\n }\n\n if (\n req.method !== Method.CREATE &&\n req.method !== Method.UPDATE &&\n req.method !== Method.DELETE\n ) {\n throw new ConnectError('operation method is invalid', Code.InvalidArgument)\n }\n\n if (req.method === Method.CREATE || req.method === Method.UPDATE) {\n try {\n JSON.parse(new TextDecoder().decode(req.payload))\n } catch (error) {\n throw new ConnectError(\n 'payload must be a valid JSON when method is CREATE or UPDATE',\n Code.InvalidArgument,\n )\n }\n }\n\n if (req.method === Method.DELETE && req.payload.length > 0) {\n throw new ConnectError(\n 'cannot specify a payload when method is DELETE',\n Code.InvalidArgument,\n )\n }\n\n return req as Operation\n}\n\nconst validateNamespace = (namespace: string): void => {\n const parts = namespace.split('#')\n\n if (parts.length !== 1 && parts.length !== 2) {\n throw new Error('namespace must be in the format \"nsid[#fragment]\"')\n }\n\n const [nsid, fragment] = parts\n\n ensureValidNsid(nsid)\n if (fragment && !/^[a-zA-Z][a-zA-Z0-9]*$/.test(fragment)) {\n throw new Error('namespace fragment must be a valid identifier')\n }\n}\n\ntype Operation = {\n actorDid: string\n namespace: string\n key: string\n payload: Uint8Array\n method: OperationMethod\n}\n"]}
1
+ {"version":3,"file":"put-operation.js","sourceRoot":"","sources":["../../src/routes/put-operation.ts"],"names":[],"mappings":"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","sourcesContent":["import { Code, ConnectError, ServiceImpl } from '@connectrpc/connect'\nimport { sql } from 'kysely'\nimport { ensureValidRecordKey } from '@atproto/syntax'\nimport { AppContext } from '../context'\nimport { Database } from '../db'\nimport { OperationMethod, createOperationChannel } from '../db/schema/operation'\nimport { Service } from '../proto/bsync_connect'\nimport {\n Method,\n PutOperationRequest,\n PutOperationResponse,\n} from '../proto/bsync_pb'\nimport { authWithApiKey } from './auth'\nimport { isValidDid, validateNamespace } from './util'\n\nexport default (ctx: AppContext): Partial<ServiceImpl<typeof Service>> => ({\n async putOperation(req, handlerCtx) {\n authWithApiKey(ctx, handlerCtx)\n const { db } = ctx\n const op = validateOp(req)\n const id = await db.transaction(async (txn) => {\n return putOp(txn, op)\n })\n return new PutOperationResponse({\n operation: {\n id: String(id),\n actorDid: op.actorDid,\n namespace: op.namespace,\n key: op.key,\n method: op.method,\n payload: op.payload,\n },\n })\n },\n})\n\nconst putOp = async (db: Database, op: Operation) => {\n const { ref } = db.db.dynamic\n const { id } = await db.db\n .insertInto('operation')\n .values({\n actorDid: op.actorDid,\n namespace: op.namespace,\n key: op.key,\n method: op.method,\n payload: op.payload,\n })\n .returning('id')\n .executeTakeFirstOrThrow()\n await sql`notify ${ref(createOperationChannel)}`.execute(db.db) // emitted transactionally\n return id\n}\n\nconst validateOp = (req: PutOperationRequest): Operation => {\n try {\n validateNamespace(req.namespace)\n } catch (error) {\n throw new ConnectError(\n 'operation namespace is invalid NSID',\n Code.InvalidArgument,\n )\n }\n\n if (!isValidDid(req.actorDid)) {\n throw new ConnectError(\n 'operation actor_did is invalid DID',\n Code.InvalidArgument,\n )\n }\n\n try {\n ensureValidRecordKey(req.key)\n } catch (error) {\n throw new ConnectError('operation key is required', Code.InvalidArgument)\n }\n\n if (\n req.method !== Method.CREATE &&\n req.method !== Method.UPDATE &&\n req.method !== Method.DELETE\n ) {\n throw new ConnectError('operation method is invalid', Code.InvalidArgument)\n }\n\n if (req.method === Method.CREATE || req.method === Method.UPDATE) {\n try {\n JSON.parse(new TextDecoder().decode(req.payload))\n } catch (error) {\n throw new ConnectError(\n 'payload must be a valid JSON when method is CREATE or UPDATE',\n Code.InvalidArgument,\n )\n }\n }\n\n if (req.method === Method.DELETE && req.payload.length > 0) {\n throw new ConnectError(\n 'cannot specify a payload when method is DELETE',\n Code.InvalidArgument,\n )\n }\n\n return req as Operation\n}\n\ntype Operation = {\n actorDid: string\n namespace: string\n key: string\n payload: Uint8Array\n method: OperationMethod\n}\n"]}
@@ -2,4 +2,5 @@ export declare const validCursor: (cursor: string) => number | null;
2
2
  export declare const combineSignals: (a: AbortSignal, b: AbortSignal) => AbortSignal;
3
3
  export declare const isValidDid: (did: string) => boolean;
4
4
  export declare const isValidAtUri: (uri: string) => boolean;
5
+ export declare const validateNamespace: (namespace: string) => void;
5
6
  //# sourceMappingURL=util.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../../src/routes/util.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,WAAW,GAAI,QAAQ,MAAM,KAAG,MAAM,GAAG,IAOrD,CAAA;AAED,eAAO,MAAM,cAAc,GAAI,GAAG,WAAW,EAAE,GAAG,WAAW,gBAa5D,CAAA;AAED,eAAO,MAAM,UAAU,GAAI,KAAK,MAAM,YAUrC,CAAA;AAED,eAAO,MAAM,YAAY,GAAI,KAAK,MAAM,YAOvC,CAAA"}
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../../src/routes/util.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,WAAW,GAAI,QAAQ,MAAM,KAAG,MAAM,GAAG,IAOrD,CAAA;AAED,eAAO,MAAM,cAAc,GAAI,GAAG,WAAW,EAAE,GAAG,WAAW,gBAa5D,CAAA;AAED,eAAO,MAAM,UAAU,GAAI,KAAK,MAAM,YAUrC,CAAA;AAED,eAAO,MAAM,YAAY,GAAI,KAAK,MAAM,YAOvC,CAAA;AAED,eAAO,MAAM,iBAAiB,GAAI,WAAW,MAAM,KAAG,IAarD,CAAA"}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isValidAtUri = exports.isValidDid = exports.combineSignals = exports.validCursor = void 0;
3
+ exports.validateNamespace = exports.isValidAtUri = exports.isValidDid = exports.combineSignals = exports.validCursor = void 0;
4
4
  const connect_1 = require("@connectrpc/connect");
5
5
  const syntax_1 = require("@atproto/syntax");
6
6
  const validCursor = (cursor) => {
@@ -51,4 +51,16 @@ const isValidAtUri = (uri) => {
51
51
  }
52
52
  };
53
53
  exports.isValidAtUri = isValidAtUri;
54
+ const validateNamespace = (namespace) => {
55
+ const parts = namespace.split('#');
56
+ if (parts.length !== 1 && parts.length !== 2) {
57
+ throw new Error('namespace must be in the format "nsid[#fragment]"');
58
+ }
59
+ const [nsid, fragment] = parts;
60
+ (0, syntax_1.ensureValidNsid)(nsid);
61
+ if (fragment && !/^[a-zA-Z][a-zA-Z0-9]*$/.test(fragment)) {
62
+ throw new Error('namespace fragment must be a valid identifier');
63
+ }
64
+ };
65
+ exports.validateNamespace = validateNamespace;
54
66
  //# sourceMappingURL=util.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"util.js","sourceRoot":"","sources":["../../src/routes/util.ts"],"names":[],"mappings":";;;AAAA,iDAAwD;AACxD,4CAIwB;AAEjB,MAAM,WAAW,GAAG,CAAC,MAAc,EAAiB,EAAE;IAC3D,IAAI,MAAM,KAAK,EAAE;QAAE,OAAO,IAAI,CAAA;IAC9B,MAAM,GAAG,GAAG,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;IAChC,IAAI,KAAK,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,sBAAY,CAAC,gBAAgB,EAAE,cAAI,CAAC,eAAe,CAAC,CAAA;IAChE,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC,CAAA;AAPY,QAAA,WAAW,eAOvB;AAEM,MAAM,cAAc,GAAG,CAAC,CAAc,EAAE,CAAc,EAAE,EAAE;IAC/D,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAA;IACxC,KAAK,MAAM,MAAM,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;QAC5B,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACnB,UAAU,CAAC,KAAK,EAAE,CAAA;YAClB,OAAO,MAAM,CAAA;QACf,CAAC;QACD,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE;YACtE,2EAA2E;YAC3E,MAAM,EAAE,UAAU,CAAC,MAAM;SAC1B,CAAC,CAAA;IACJ,CAAC;IACD,OAAO,UAAU,CAAC,MAAM,CAAA;AAC1B,CAAC,CAAA;AAbY,QAAA,cAAc,kBAa1B;AAEM,MAAM,UAAU,GAAG,CAAC,GAAW,EAAE,EAAE;IACxC,IAAI,CAAC;QACH,IAAA,uBAAc,EAAC,GAAG,CAAC,CAAA;QACnB,OAAO,IAAI,CAAA;IACb,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,wBAAe,EAAE,CAAC;YACnC,OAAO,KAAK,CAAA;QACd,CAAC;QACD,MAAM,GAAG,CAAA;IACX,CAAC;AACH,CAAC,CAAA;AAVY,QAAA,UAAU,cAUtB;AAEM,MAAM,YAAY,GAAG,CAAC,GAAW,EAAE,EAAE;IAC1C,IAAI,CAAC;QACH,IAAA,yBAAgB,EAAC,GAAG,CAAC,CAAA;QACrB,OAAO,IAAI,CAAA;IACb,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC,CAAA;AAPY,QAAA,YAAY,gBAOxB","sourcesContent":["import { Code, ConnectError } from '@connectrpc/connect'\nimport {\n InvalidDidError,\n ensureValidAtUri,\n ensureValidDid,\n} from '@atproto/syntax'\n\nexport const validCursor = (cursor: string): number | null => {\n if (cursor === '') return null\n const int = parseInt(cursor, 10)\n if (isNaN(int) || int < 0) {\n throw new ConnectError('invalid cursor', Code.InvalidArgument)\n }\n return int\n}\n\nexport const combineSignals = (a: AbortSignal, b: AbortSignal) => {\n const controller = new AbortController()\n for (const signal of [a, b]) {\n if (signal.aborted) {\n controller.abort()\n return signal\n }\n signal.addEventListener('abort', () => controller.abort(signal.reason), {\n // @ts-ignore https://github.com/DefinitelyTyped/DefinitelyTyped/pull/68625\n signal: controller.signal,\n })\n }\n return controller.signal\n}\n\nexport const isValidDid = (did: string) => {\n try {\n ensureValidDid(did)\n return true\n } catch (err) {\n if (err instanceof InvalidDidError) {\n return false\n }\n throw err\n }\n}\n\nexport const isValidAtUri = (uri: string) => {\n try {\n ensureValidAtUri(uri)\n return true\n } catch {\n return false\n }\n}\n"]}
1
+ {"version":3,"file":"util.js","sourceRoot":"","sources":["../../src/routes/util.ts"],"names":[],"mappings":";;;AAAA,iDAAwD;AACxD,4CAKwB;AAEjB,MAAM,WAAW,GAAG,CAAC,MAAc,EAAiB,EAAE;IAC3D,IAAI,MAAM,KAAK,EAAE;QAAE,OAAO,IAAI,CAAA;IAC9B,MAAM,GAAG,GAAG,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;IAChC,IAAI,KAAK,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,sBAAY,CAAC,gBAAgB,EAAE,cAAI,CAAC,eAAe,CAAC,CAAA;IAChE,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC,CAAA;AAPY,QAAA,WAAW,eAOvB;AAEM,MAAM,cAAc,GAAG,CAAC,CAAc,EAAE,CAAc,EAAE,EAAE;IAC/D,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAA;IACxC,KAAK,MAAM,MAAM,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;QAC5B,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACnB,UAAU,CAAC,KAAK,EAAE,CAAA;YAClB,OAAO,MAAM,CAAA;QACf,CAAC;QACD,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE;YACtE,2EAA2E;YAC3E,MAAM,EAAE,UAAU,CAAC,MAAM;SAC1B,CAAC,CAAA;IACJ,CAAC;IACD,OAAO,UAAU,CAAC,MAAM,CAAA;AAC1B,CAAC,CAAA;AAbY,QAAA,cAAc,kBAa1B;AAEM,MAAM,UAAU,GAAG,CAAC,GAAW,EAAE,EAAE;IACxC,IAAI,CAAC;QACH,IAAA,uBAAc,EAAC,GAAG,CAAC,CAAA;QACnB,OAAO,IAAI,CAAA;IACb,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,wBAAe,EAAE,CAAC;YACnC,OAAO,KAAK,CAAA;QACd,CAAC;QACD,MAAM,GAAG,CAAA;IACX,CAAC;AACH,CAAC,CAAA;AAVY,QAAA,UAAU,cAUtB;AAEM,MAAM,YAAY,GAAG,CAAC,GAAW,EAAE,EAAE;IAC1C,IAAI,CAAC;QACH,IAAA,yBAAgB,EAAC,GAAG,CAAC,CAAA;QACrB,OAAO,IAAI,CAAA;IACb,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC,CAAA;AAPY,QAAA,YAAY,gBAOxB;AAEM,MAAM,iBAAiB,GAAG,CAAC,SAAiB,EAAQ,EAAE;IAC3D,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAElC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC7C,MAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,CAAA;IACtE,CAAC;IAED,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,GAAG,KAAK,CAAA;IAE9B,IAAA,wBAAe,EAAC,IAAI,CAAC,CAAA;IACrB,IAAI,QAAQ,IAAI,CAAC,wBAAwB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QACzD,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAA;IAClE,CAAC;AACH,CAAC,CAAA;AAbY,QAAA,iBAAiB,qBAa7B","sourcesContent":["import { Code, ConnectError } from '@connectrpc/connect'\nimport {\n InvalidDidError,\n ensureValidAtUri,\n ensureValidDid,\n ensureValidNsid,\n} from '@atproto/syntax'\n\nexport const validCursor = (cursor: string): number | null => {\n if (cursor === '') return null\n const int = parseInt(cursor, 10)\n if (isNaN(int) || int < 0) {\n throw new ConnectError('invalid cursor', Code.InvalidArgument)\n }\n return int\n}\n\nexport const combineSignals = (a: AbortSignal, b: AbortSignal) => {\n const controller = new AbortController()\n for (const signal of [a, b]) {\n if (signal.aborted) {\n controller.abort()\n return signal\n }\n signal.addEventListener('abort', () => controller.abort(signal.reason), {\n // @ts-ignore https://github.com/DefinitelyTyped/DefinitelyTyped/pull/68625\n signal: controller.signal,\n })\n }\n return controller.signal\n}\n\nexport const isValidDid = (did: string) => {\n try {\n ensureValidDid(did)\n return true\n } catch (err) {\n if (err instanceof InvalidDidError) {\n return false\n }\n throw err\n }\n}\n\nexport const isValidAtUri = (uri: string) => {\n try {\n ensureValidAtUri(uri)\n return true\n } catch {\n return false\n }\n}\n\nexport const validateNamespace = (namespace: string): void => {\n const parts = namespace.split('#')\n\n if (parts.length !== 1 && parts.length !== 2) {\n throw new Error('namespace must be in the format \"nsid[#fragment]\"')\n }\n\n const [nsid, fragment] = parts\n\n ensureValidNsid(nsid)\n if (fragment && !/^[a-zA-Z][a-zA-Z0-9]*$/.test(fragment)) {\n throw new Error('namespace fragment must be a valid identifier')\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/bsync",
3
- "version": "0.0.23",
3
+ "version": "0.0.25",
4
4
  "license": "MIT",
5
5
  "description": "Sychronizing service for app.bsky App View (Bluesky API)",
6
6
  "keywords": [
@@ -27,8 +27,8 @@
27
27
  "pg": "^8.10.0",
28
28
  "pino-http": "^8.2.1",
29
29
  "typed-emitter": "^2.1.0",
30
- "@atproto/common": "^0.5.0",
31
- "@atproto/syntax": "^0.4.1"
30
+ "@atproto/common": "^0.5.14",
31
+ "@atproto/syntax": "^0.5.1"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@bufbuild/buf": "^1.28.1",
package/proto/bsync.proto CHANGED
@@ -105,6 +105,15 @@ message ScanOperationsResponse {
105
105
  string cursor = 2;
106
106
  }
107
107
 
108
+ message DeleteOperationsByActorAndNamespaceRequest {
109
+ string actor_did = 1;
110
+ string namespace = 2;
111
+ }
112
+
113
+ message DeleteOperationsByActorAndNamespaceResponse {
114
+ int32 deleted_count = 1;
115
+ }
116
+
108
117
 
109
118
  // Ping
110
119
  message PingRequest {}
@@ -119,6 +128,7 @@ service Service {
119
128
  rpc ScanNotifOperations(ScanNotifOperationsRequest) returns (ScanNotifOperationsResponse);
120
129
  rpc PutOperation(PutOperationRequest) returns (PutOperationResponse);
121
130
  rpc ScanOperations(ScanOperationsRequest) returns (ScanOperationsResponse);
131
+ rpc DeleteOperationsByActorAndNamespace(DeleteOperationsByActorAndNamespaceRequest) returns (DeleteOperationsByActorAndNamespaceResponse);
122
132
  // Ping
123
133
  rpc Ping(PingRequest) returns (PingResponse);
124
134
  }
@@ -3,7 +3,7 @@
3
3
  /* eslint-disable */
4
4
  // @ts-nocheck
5
5
 
6
- import { AddMuteOperationRequest, AddMuteOperationResponse, AddNotifOperationRequest, AddNotifOperationResponse, PingRequest, PingResponse, PutOperationRequest, PutOperationResponse, ScanMuteOperationsRequest, ScanMuteOperationsResponse, ScanNotifOperationsRequest, ScanNotifOperationsResponse, ScanOperationsRequest, ScanOperationsResponse } from "./bsync_pb";
6
+ import { AddMuteOperationRequest, AddMuteOperationResponse, AddNotifOperationRequest, AddNotifOperationResponse, DeleteOperationsByActorAndNamespaceRequest, DeleteOperationsByActorAndNamespaceResponse, PingRequest, PingResponse, PutOperationRequest, PutOperationResponse, ScanMuteOperationsRequest, ScanMuteOperationsResponse, ScanNotifOperationsRequest, ScanNotifOperationsResponse, ScanOperationsRequest, ScanOperationsResponse } from "./bsync_pb";
7
7
  import { MethodKind } from "@bufbuild/protobuf";
8
8
 
9
9
  /**
@@ -68,6 +68,15 @@ export const Service = {
68
68
  O: ScanOperationsResponse,
69
69
  kind: MethodKind.Unary,
70
70
  },
71
+ /**
72
+ * @generated from rpc bsync.Service.DeleteOperationsByActorAndNamespace
73
+ */
74
+ deleteOperationsByActorAndNamespace: {
75
+ name: "DeleteOperationsByActorAndNamespace",
76
+ I: DeleteOperationsByActorAndNamespaceRequest,
77
+ O: DeleteOperationsByActorAndNamespaceResponse,
78
+ kind: MethodKind.Unary,
79
+ },
71
80
  /**
72
81
  * Ping
73
82
  *
@@ -763,6 +763,86 @@ export class ScanOperationsResponse extends Message<ScanOperationsResponse> {
763
763
  }
764
764
  }
765
765
 
766
+ /**
767
+ * @generated from message bsync.DeleteOperationsByActorAndNamespaceRequest
768
+ */
769
+ export class DeleteOperationsByActorAndNamespaceRequest extends Message<DeleteOperationsByActorAndNamespaceRequest> {
770
+ /**
771
+ * @generated from field: string actor_did = 1;
772
+ */
773
+ actorDid = "";
774
+
775
+ /**
776
+ * @generated from field: string namespace = 2;
777
+ */
778
+ namespace = "";
779
+
780
+ constructor(data?: PartialMessage<DeleteOperationsByActorAndNamespaceRequest>) {
781
+ super();
782
+ proto3.util.initPartial(data, this);
783
+ }
784
+
785
+ static readonly runtime: typeof proto3 = proto3;
786
+ static readonly typeName = "bsync.DeleteOperationsByActorAndNamespaceRequest";
787
+ static readonly fields: FieldList = proto3.util.newFieldList(() => [
788
+ { no: 1, name: "actor_did", kind: "scalar", T: 9 /* ScalarType.STRING */ },
789
+ { no: 2, name: "namespace", kind: "scalar", T: 9 /* ScalarType.STRING */ },
790
+ ]);
791
+
792
+ static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): DeleteOperationsByActorAndNamespaceRequest {
793
+ return new DeleteOperationsByActorAndNamespaceRequest().fromBinary(bytes, options);
794
+ }
795
+
796
+ static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): DeleteOperationsByActorAndNamespaceRequest {
797
+ return new DeleteOperationsByActorAndNamespaceRequest().fromJson(jsonValue, options);
798
+ }
799
+
800
+ static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): DeleteOperationsByActorAndNamespaceRequest {
801
+ return new DeleteOperationsByActorAndNamespaceRequest().fromJsonString(jsonString, options);
802
+ }
803
+
804
+ static equals(a: DeleteOperationsByActorAndNamespaceRequest | PlainMessage<DeleteOperationsByActorAndNamespaceRequest> | undefined, b: DeleteOperationsByActorAndNamespaceRequest | PlainMessage<DeleteOperationsByActorAndNamespaceRequest> | undefined): boolean {
805
+ return proto3.util.equals(DeleteOperationsByActorAndNamespaceRequest, a, b);
806
+ }
807
+ }
808
+
809
+ /**
810
+ * @generated from message bsync.DeleteOperationsByActorAndNamespaceResponse
811
+ */
812
+ export class DeleteOperationsByActorAndNamespaceResponse extends Message<DeleteOperationsByActorAndNamespaceResponse> {
813
+ /**
814
+ * @generated from field: int32 deleted_count = 1;
815
+ */
816
+ deletedCount = 0;
817
+
818
+ constructor(data?: PartialMessage<DeleteOperationsByActorAndNamespaceResponse>) {
819
+ super();
820
+ proto3.util.initPartial(data, this);
821
+ }
822
+
823
+ static readonly runtime: typeof proto3 = proto3;
824
+ static readonly typeName = "bsync.DeleteOperationsByActorAndNamespaceResponse";
825
+ static readonly fields: FieldList = proto3.util.newFieldList(() => [
826
+ { no: 1, name: "deleted_count", kind: "scalar", T: 5 /* ScalarType.INT32 */ },
827
+ ]);
828
+
829
+ static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): DeleteOperationsByActorAndNamespaceResponse {
830
+ return new DeleteOperationsByActorAndNamespaceResponse().fromBinary(bytes, options);
831
+ }
832
+
833
+ static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): DeleteOperationsByActorAndNamespaceResponse {
834
+ return new DeleteOperationsByActorAndNamespaceResponse().fromJson(jsonValue, options);
835
+ }
836
+
837
+ static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): DeleteOperationsByActorAndNamespaceResponse {
838
+ return new DeleteOperationsByActorAndNamespaceResponse().fromJsonString(jsonString, options);
839
+ }
840
+
841
+ static equals(a: DeleteOperationsByActorAndNamespaceResponse | PlainMessage<DeleteOperationsByActorAndNamespaceResponse> | undefined, b: DeleteOperationsByActorAndNamespaceResponse | PlainMessage<DeleteOperationsByActorAndNamespaceResponse> | undefined): boolean {
842
+ return proto3.util.equals(DeleteOperationsByActorAndNamespaceResponse, a, b);
843
+ }
844
+ }
845
+
766
846
  /**
767
847
  * Ping
768
848
  *
@@ -0,0 +1,45 @@
1
+ import { Code, ConnectError, ServiceImpl } from '@connectrpc/connect'
2
+ import { AppContext } from '../context'
3
+ import { Service } from '../proto/bsync_connect'
4
+ import { DeleteOperationsByActorAndNamespaceResponse } from '../proto/bsync_pb'
5
+ import { authWithApiKey } from './auth'
6
+ import { isValidDid, validateNamespace } from './util'
7
+
8
+ export default (ctx: AppContext): Partial<ServiceImpl<typeof Service>> => ({
9
+ /**
10
+ * This method is responsible for deleting log rows from the bsync db, it has
11
+ * no other downstream effects. This method is called from the dataplane in
12
+ * response to a data deletion request initiated by a moderator in Ozone.
13
+ * It's the final step of the deletion process, basically cleaning up the
14
+ * breadcrumbs that resulted in the state we store in the dataplane.
15
+ */
16
+ async deleteOperationsByActorAndNamespace(req, handlerCtx) {
17
+ authWithApiKey(ctx, handlerCtx)
18
+ const { db } = ctx
19
+
20
+ try {
21
+ validateNamespace(req.namespace)
22
+ } catch (error) {
23
+ throw new ConnectError(
24
+ 'requested namespace for deletion is invalid NSID',
25
+ Code.InvalidArgument,
26
+ )
27
+ }
28
+ if (!isValidDid(req.actorDid)) {
29
+ throw new ConnectError(
30
+ 'requested actor_did for deletion is invalid DID',
31
+ Code.InvalidArgument,
32
+ )
33
+ }
34
+
35
+ const deletedRows = await db.db
36
+ .deleteFrom('operation')
37
+ .where('actorDid', '=', req.actorDid)
38
+ .where('namespace', '=', req.namespace)
39
+ .returning('id')
40
+ .execute()
41
+ return new DeleteOperationsByActorAndNamespaceResponse({
42
+ deletedCount: deletedRows.length,
43
+ })
44
+ },
45
+ })
@@ -4,6 +4,7 @@ import { AppContext } from '../context'
4
4
  import { Service } from '../proto/bsync_connect'
5
5
  import addMuteOperation from './add-mute-operation'
6
6
  import addNotifOperation from './add-notif-operation'
7
+ import deleteOperations from './delete-operations'
7
8
  import putOperation from './put-operation'
8
9
  import scanMuteOperations from './scan-mute-operations'
9
10
  import scanNotifOperations from './scan-notif-operations'
@@ -17,6 +18,7 @@ export default (ctx: AppContext) => (router: ConnectRouter) => {
17
18
  ...scanNotifOperations(ctx),
18
19
  ...putOperation(ctx),
19
20
  ...scanOperations(ctx),
21
+ ...deleteOperations(ctx),
20
22
  async ping() {
21
23
  const { db } = ctx
22
24
  await sql`select 1`.execute(db.db)
@@ -1,6 +1,6 @@
1
1
  import { Code, ConnectError, ServiceImpl } from '@connectrpc/connect'
2
2
  import { sql } from 'kysely'
3
- import { ensureValidNsid, ensureValidRecordKey } from '@atproto/syntax'
3
+ import { ensureValidRecordKey } from '@atproto/syntax'
4
4
  import { AppContext } from '../context'
5
5
  import { Database } from '../db'
6
6
  import { OperationMethod, createOperationChannel } from '../db/schema/operation'
@@ -11,7 +11,7 @@ import {
11
11
  PutOperationResponse,
12
12
  } from '../proto/bsync_pb'
13
13
  import { authWithApiKey } from './auth'
14
- import { isValidDid } from './util'
14
+ import { isValidDid, validateNamespace } from './util'
15
15
 
16
16
  export default (ctx: AppContext): Partial<ServiceImpl<typeof Service>> => ({
17
17
  async putOperation(req, handlerCtx) {
@@ -103,21 +103,6 @@ const validateOp = (req: PutOperationRequest): Operation => {
103
103
  return req as Operation
104
104
  }
105
105
 
106
- const validateNamespace = (namespace: string): void => {
107
- const parts = namespace.split('#')
108
-
109
- if (parts.length !== 1 && parts.length !== 2) {
110
- throw new Error('namespace must be in the format "nsid[#fragment]"')
111
- }
112
-
113
- const [nsid, fragment] = parts
114
-
115
- ensureValidNsid(nsid)
116
- if (fragment && !/^[a-zA-Z][a-zA-Z0-9]*$/.test(fragment)) {
117
- throw new Error('namespace fragment must be a valid identifier')
118
- }
119
- }
120
-
121
106
  type Operation = {
122
107
  actorDid: string
123
108
  namespace: string
@@ -3,6 +3,7 @@ import {
3
3
  InvalidDidError,
4
4
  ensureValidAtUri,
5
5
  ensureValidDid,
6
+ ensureValidNsid,
6
7
  } from '@atproto/syntax'
7
8
 
8
9
  export const validCursor = (cursor: string): number | null => {
@@ -49,3 +50,18 @@ export const isValidAtUri = (uri: string) => {
49
50
  return false
50
51
  }
51
52
  }
53
+
54
+ export const validateNamespace = (namespace: string): void => {
55
+ const parts = namespace.split('#')
56
+
57
+ if (parts.length !== 1 && parts.length !== 2) {
58
+ throw new Error('namespace must be in the format "nsid[#fragment]"')
59
+ }
60
+
61
+ const [nsid, fragment] = parts
62
+
63
+ ensureValidNsid(nsid)
64
+ if (fragment && !/^[a-zA-Z][a-zA-Z0-9]*$/.test(fragment)) {
65
+ throw new Error('namespace fragment must be a valid identifier')
66
+ }
67
+ }
@@ -0,0 +1,108 @@
1
+ import getPort from 'get-port'
2
+ import {
3
+ BsyncClient,
4
+ BsyncService,
5
+ Database,
6
+ authWithApiKey,
7
+ createClient,
8
+ envToCfg,
9
+ } from '../src'
10
+ import { Method } from '../src/proto/bsync_pb'
11
+
12
+ describe('operations', () => {
13
+ let bsync: BsyncService
14
+ let client: BsyncClient
15
+
16
+ const validPayload0 = Buffer.from(JSON.stringify({ value: 0 }))
17
+ const validPayload1 = Buffer.from(JSON.stringify({ value: 1 }))
18
+
19
+ beforeAll(async () => {
20
+ bsync = await BsyncService.create(
21
+ envToCfg({
22
+ port: await getPort(),
23
+ dbUrl: process.env.DB_POSTGRES_URL,
24
+ dbSchema: 'bsync_delete_operations',
25
+ apiKeys: ['key-1'],
26
+ longPollTimeoutMs: 500,
27
+ }),
28
+ )
29
+ await bsync.ctx.db.migrateToLatestOrThrow()
30
+ await bsync.start()
31
+ client = createClient({
32
+ httpVersion: '1.1',
33
+ baseUrl: `http://localhost:${bsync.ctx.cfg.service.port}`,
34
+ interceptors: [authWithApiKey('key-1')],
35
+ })
36
+ })
37
+
38
+ afterAll(async () => {
39
+ await bsync.destroy()
40
+ })
41
+
42
+ beforeEach(async () => {
43
+ await clearOps(bsync.ctx.db)
44
+ })
45
+
46
+ it('deletes', async () => {
47
+ const res1 = await client.putOperation({
48
+ actorDid: 'did:example:a',
49
+ namespace: 'app.bsky.some.col',
50
+ key: 'key1',
51
+ method: Method.CREATE,
52
+ payload: validPayload0,
53
+ })
54
+ const res2 = await client.putOperation({
55
+ actorDid: 'did:example:a',
56
+ namespace: 'app.bsky.other.col#id',
57
+ key: 'key1',
58
+ method: Method.UPDATE,
59
+ payload: validPayload1,
60
+ })
61
+
62
+ expect(res1.operation?.id).toBe('1')
63
+ expect(res2.operation?.id).toBe('2')
64
+ expect(await dumpOps(bsync.ctx.db)).toStrictEqual([
65
+ {
66
+ id: 1,
67
+ actorDid: 'did:example:a',
68
+ namespace: 'app.bsky.some.col',
69
+ key: 'key1',
70
+ method: Method.CREATE,
71
+ payload: validPayload0,
72
+ createdAt: expect.any(Date),
73
+ },
74
+ {
75
+ id: 2,
76
+ actorDid: 'did:example:a',
77
+ namespace: 'app.bsky.other.col#id',
78
+ key: 'key1',
79
+ method: Method.UPDATE,
80
+ payload: validPayload1,
81
+ createdAt: expect.any(Date),
82
+ },
83
+ ])
84
+
85
+ await client.deleteOperationsByActorAndNamespace({
86
+ actorDid: 'did:example:a',
87
+ namespace: 'app.bsky.some.col',
88
+ })
89
+ await client.deleteOperationsByActorAndNamespace({
90
+ actorDid: 'did:example:a',
91
+ namespace: 'app.bsky.other.col#id',
92
+ })
93
+
94
+ expect(await dumpOps(bsync.ctx.db)).toStrictEqual([])
95
+ })
96
+ })
97
+
98
+ const dumpOps = async (db: Database) => {
99
+ return db.db
100
+ .selectFrom('operation')
101
+ .selectAll()
102
+ .orderBy('id', 'asc')
103
+ .execute()
104
+ }
105
+
106
+ const clearOps = async (db: Database) => {
107
+ await db.db.deleteFrom('operation').execute()
108
+ }
@@ -1 +1 @@
1
- {"root":["./src/client.ts","./src/config.ts","./src/context.ts","./src/index.ts","./src/logger.ts","./src/db/index.ts","./src/db/types.ts","./src/db/migrations/20240108T220751294Z-init.ts","./src/db/migrations/20240717T224303472Z-notif-ops.ts","./src/db/migrations/20250527T022203400Z-add-operation.ts","./src/db/migrations/20250603T163446567Z-alter-operation.ts","./src/db/migrations/index.ts","./src/db/migrations/provider.ts","./src/db/schema/index.ts","./src/db/schema/mute_item.ts","./src/db/schema/mute_op.ts","./src/db/schema/notif_item.ts","./src/db/schema/notif_op.ts","./src/db/schema/operation.ts","./src/proto/bsync_connect.ts","./src/proto/bsync_pb.ts","./src/routes/add-mute-operation.ts","./src/routes/add-notif-operation.ts","./src/routes/auth.ts","./src/routes/index.ts","./src/routes/put-operation.ts","./src/routes/scan-mute-operations.ts","./src/routes/scan-notif-operations.ts","./src/routes/scan-operations.ts","./src/routes/util.ts"],"version":"5.8.2"}
1
+ {"root":["./src/client.ts","./src/config.ts","./src/context.ts","./src/index.ts","./src/logger.ts","./src/db/index.ts","./src/db/types.ts","./src/db/migrations/20240108T220751294Z-init.ts","./src/db/migrations/20240717T224303472Z-notif-ops.ts","./src/db/migrations/20250527T022203400Z-add-operation.ts","./src/db/migrations/20250603T163446567Z-alter-operation.ts","./src/db/migrations/index.ts","./src/db/migrations/provider.ts","./src/db/schema/index.ts","./src/db/schema/mute_item.ts","./src/db/schema/mute_op.ts","./src/db/schema/notif_item.ts","./src/db/schema/notif_op.ts","./src/db/schema/operation.ts","./src/proto/bsync_connect.ts","./src/proto/bsync_pb.ts","./src/routes/add-mute-operation.ts","./src/routes/add-notif-operation.ts","./src/routes/auth.ts","./src/routes/delete-operations.ts","./src/routes/index.ts","./src/routes/put-operation.ts","./src/routes/scan-mute-operations.ts","./src/routes/scan-notif-operations.ts","./src/routes/scan-operations.ts","./src/routes/util.ts"],"version":"5.8.2"}
@@ -1 +0,0 @@
1
- {"root":["./tests/mutes.test.ts","./tests/notifications.test.ts","./tests/operations.test.ts"],"version":"5.8.3"}