@the_pixelport/aws-lambda-graphql
Version:
Apollo server for AWS Lambda with WebSocket subscriptions support over API Gateway v1 + v2
244 lines • 11.6 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DynamoDBSubscriptionManager = void 0;
const assert_1 = __importDefault(require("assert"));
const client_dynamodb_1 = require("@aws-sdk/client-dynamodb");
const lib_dynamodb_1 = require("@aws-sdk/lib-dynamodb");
const helpers_1 = require("./helpers");
const DEFAULT_TTL = 7200;
// polyfill Symbol.asyncIterator
if (Symbol.asyncIterator === undefined) {
Symbol.asyncIterator = Symbol.for('asyncIterator');
}
/**
* DynamoDBSubscriptionManager
*
* Stores all subsrciptions in Subscriptions and SubscriptionOperations tables (both can be overridden)
*
* DynamoDB table structures
*
* Subscriptions:
* event: primary key (HASH)
* subscriptionId: range key (RANGE) - connectionId:operationId (this is always unique per client)
*
* SubscriptionOperations:
* subscriptionId: primary key (HASH) - connectionId:operationId (this is always unique per client)
*/
class DynamoDBSubscriptionManager {
constructor({ dynamoDbClient, subscriptionsTableName = 'Subscriptions', subscriptionOperationsTableName = 'SubscriptionOperations', ttl = DEFAULT_TTL, getSubscriptionNameFromEvent = (event) => event.event, getSubscriptionNameFromConnection = (name) => name, } = {}) {
this.subscribersByEvent = (event) => {
let ExclusiveStartKey;
let done = false;
const name = this.getSubscriptionNameFromEvent(event);
return {
next: async () => {
if (done) {
return { value: [], done: true };
}
const time = Math.round(Date.now() / 1000);
const result = await this.db.send(new lib_dynamodb_1.QueryCommand({
ExclusiveStartKey,
TableName: this.subscriptionsTableName,
Limit: 50,
KeyConditionExpression: 'event = :event',
FilterExpression: '#ttl > :time OR attribute_not_exists(#ttl)',
ExpressionAttributeValues: {
':event': name,
':time': time,
},
ExpressionAttributeNames: {
'#ttl': 'ttl',
},
}));
ExclusiveStartKey = result.LastEvaluatedKey;
if (ExclusiveStartKey == null) {
done = true;
}
// we store connectionData on subscription too so we don't
// need to load data from connections table
const value = (result.Items || []);
return { value, done: done && value.length === 0 };
},
[Symbol.asyncIterator]() {
return this;
},
};
};
this.subscribe = async (names, connection, operation) => {
const subscriptionId = this.generateSubscriptionId(connection.id, operation.operationId);
// we can only subscribe to one subscription in GQL document
if (names.length !== 1) {
throw new Error('Only one active operation per event name is allowed');
}
let [name] = names;
name = this.getSubscriptionNameFromConnection(name, connection);
const ttlField = this.ttl === false || this.ttl == null
? {}
: { ttl: (0, helpers_1.computeTTL)(this.ttl) };
await this.db.send(new lib_dynamodb_1.BatchWriteCommand({
RequestItems: {
[this.subscriptionsTableName]: [
{
PutRequest: {
Item: {
connection,
operation,
event: name,
subscriptionId,
operationId: operation.operationId,
...ttlField,
},
},
},
],
[this.subscriptionOperationsTableName]: [
{
PutRequest: {
Item: {
subscriptionId,
event: name,
...ttlField,
},
},
},
],
},
}));
};
this.unsubscribe = async (subscriber) => {
const subscriptionId = this.generateSubscriptionId(subscriber.connection.id, subscriber.operationId);
await this.db.send(new lib_dynamodb_1.TransactWriteCommand({
TransactItems: [
{
Delete: {
TableName: this.subscriptionsTableName,
Key: {
event: subscriber.event,
subscriptionId,
},
},
},
{
Delete: {
TableName: this.subscriptionOperationsTableName,
Key: {
subscriptionId,
},
},
},
],
}));
};
this.unsubscribeOperation = async (connectionId, operationId) => {
const operation = await this.db.send(new lib_dynamodb_1.GetCommand({
TableName: this.subscriptionOperationsTableName,
Key: {
subscriptionId: this.generateSubscriptionId(connectionId, operationId),
},
}));
if (operation.Item) {
await this.db.send(new lib_dynamodb_1.TransactWriteCommand({
TransactItems: [
{
Delete: {
TableName: this.subscriptionsTableName,
Key: {
event: operation.Item.event,
subscriptionId: operation.Item.subscriptionId,
},
},
},
{
Delete: {
TableName: this.subscriptionOperationsTableName,
Key: {
subscriptionId: operation.Item.subscriptionId,
},
},
},
],
}));
}
};
this.unsubscribeAllByConnectionId = async (connectionId) => {
let cursor;
do {
const result = await this.db.send(new lib_dynamodb_1.ScanCommand({
TableName: this.subscriptionsTableName,
ExclusiveStartKey: cursor,
FilterExpression: 'begins_with(subscriptionId, :connection_id)',
ExpressionAttributeValues: {
':connection_id': connectionId,
},
Limit: 12, // Maximum of 25 request items sent to DynamoDB a time
}));
const { Items, LastEvaluatedKey } = result;
if (Items == null || (LastEvaluatedKey == null && Items.length === 0)) {
return;
}
if (Items.length > 0) {
await this.db.send(new lib_dynamodb_1.BatchWriteCommand({
RequestItems: {
[this.subscriptionsTableName]: Items.map((item) => ({
DeleteRequest: {
Key: {
event: item.event,
subscriptionId: item.subscriptionId,
},
},
})),
[this.subscriptionOperationsTableName]: Items.map((item) => ({
DeleteRequest: {
Key: { subscriptionId: item.subscriptionId },
},
})),
},
}));
}
cursor = LastEvaluatedKey;
} while (cursor);
};
this.generateSubscriptionId = (connectionId, operationId) => {
return `${connectionId}:${operationId}`;
};
assert_1.default.ok(typeof subscriptionOperationsTableName === 'string', 'Please provide subscriptionOperationsTableName as a string');
assert_1.default.ok(typeof subscriptionsTableName === 'string', 'Please provide subscriptionsTableName as a string');
assert_1.default.ok(ttl === false || (typeof ttl === 'number' && ttl > 0), 'Please provide ttl as a number greater than 0 or false to turn it off');
assert_1.default.ok(dynamoDbClient == null || typeof dynamoDbClient === 'object', 'Please provide dynamoDbClient as an instance of DynamoDBDocumentClient or DynamoDBClient');
this.subscriptionsTableName = subscriptionsTableName;
this.subscriptionOperationsTableName = subscriptionOperationsTableName;
// Handle both DynamoDBDocumentClient and DynamoDBClient
// Always ensure proper marshalling options are applied
if (dynamoDbClient) {
if (dynamoDbClient instanceof lib_dynamodb_1.DynamoDBDocumentClient) {
// If a DynamoDBDocumentClient is passed, warn the user about potential marshalling issues
console.warn('[aws-lambda-graphql] Warning: DynamoDBDocumentClient passed directly. Please ensure it was created with marshallOptions: { convertClassInstanceToMap: true, removeUndefinedValues: true } to avoid marshalling errors.');
this.db = dynamoDbClient;
}
else {
this.db = lib_dynamodb_1.DynamoDBDocumentClient.from(dynamoDbClient, {
marshallOptions: {
convertClassInstanceToMap: true,
removeUndefinedValues: true,
},
});
}
}
else {
this.db = lib_dynamodb_1.DynamoDBDocumentClient.from(new client_dynamodb_1.DynamoDBClient({}), {
marshallOptions: {
convertClassInstanceToMap: true,
removeUndefinedValues: true,
},
});
}
this.ttl = ttl;
this.getSubscriptionNameFromEvent = getSubscriptionNameFromEvent;
this.getSubscriptionNameFromConnection = getSubscriptionNameFromConnection;
}
}
exports.DynamoDBSubscriptionManager = DynamoDBSubscriptionManager;
//# sourceMappingURL=DynamoDBSubscriptionManager.js.map