Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion .github/workflows/npm-publish-github-packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ on:
branches:
- main

env:
TEST_IMAGE_NAME: public.ecr.aws/lambda/nodejs:16

jobs:
test:
unit-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
Expand All @@ -19,3 +22,20 @@ jobs:
node-version: 16
- run: npm ci
- run: npm test

integration-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16

- name: Pull test docker image
run: docker pull $TEST_IMAGE_NAME

- name: Install dependencies
run: npm ci

- name: Integration test with LocalStack invoke method
run: bash ./test_in_docker.sh
279 changes: 277 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,117 @@
import { v4 as uuidv4 } from 'uuid';

import { generateFilterExpression } from "./transform/dynamodb-filter";
import { dynamodbUtils } from './dynamodb-utils';
export const dynamodbUtils = {
toDynamoDB: function(value) {
if (typeof (value) === "number") {
return this.toNumber(value);
} else if (typeof (value) === "string") {
return this.toString(value);
} else if (typeof (value) === "boolean") {
return this.toBoolean(value);
} else if (typeof (value) === "object") {
if (value.length !== undefined) {
return this.toList(value);
} else {
return this.toMap(value);
}
} else {
throw new Error(`Not implemented for ${value}`);
}
},

toString: function(value) {
if (value === null) { return null; };

return { S: value };
},

toStringSet: function(value) {
if (value === null) { return null; };

return { SS: value };
},

toNumber: function(value) {
if (value === null) { return null; };

return { N: value };
},

toNumberSet: function(value) {
if (value === null) { return null; };

return { NS: value };
},

toBinary: function(value) {
if (value === null) { return null; };

return { B: value };
},

toBinarySet: function(value) {
if (value === null) { return null; };

return { BS: value };
},

toBoolean: function(value) {
if (value === null) { return null; };

return { BOOL: value };
},

toNull: function() {
return { NULL: null };
},

toList: function(values) {
let out = [];
for (const value of values) {
out.push(this.toDynamoDB(value));
}
return { L: out }
},

toMap: function(mapping) {
return { M: this.toMapValues(mapping) };
},

toMapValues: function(mapping) {
let out = {};
for (const [k, v] of Object.entries(mapping)) {
out[k] = this.toDynamoDB(v);
}
return out;
},

toS3Object: function(key, bucket, region, version) {
let payload;
if (version === undefined) {
payload = {
s3: {
key,
bucket,
region,
}
};
} else {
payload = {
s3: {
key,
bucket,
region,
version,
}
};
};
return this.toString(JSON.stringify(payload));
},

fromS3ObjectJson: function(value) {
throw new Error("not implemented");
},
}

const FILTER_CONTAINS = "contains";

Expand Down Expand Up @@ -57,3 +167,168 @@ export const util = {
},
dynamodb: dynamodbUtils,
};

// embedded here because imports don't yet work
const OPERATOR_MAP = {
ne: '<>',
eq: '=',
lt: '<',
le: '<=',
gt: '>',
ge: '>=',
in: 'contains',
};

const FUNCTION_MAP = {
contains: 'contains',
notContains: 'NOT contains',
beginsWith: 'begins_with',
};

export function generateFilterExpression(filter, prefix, parent) {
const expr = Object.entries(filter).reduce(
(sum, [name, value]) => {
let subExpr = {
expressions: [],
expressionNames: {},
expressionValues: {},
};
const fieldName = createExpressionFieldName(parent);
const filedValueName = createExpressionValueName(parent, name, prefix);

switch (name) {
case 'or':
case 'and': {
const JOINER = name === 'or' ? 'OR' : 'AND';
if (Array.isArray(value)) {
subExpr = scopeExpression(
value.reduce((expr, subFilter, idx) => {
const newExpr = generateFilterExpression(subFilter, [prefix, name, idx].filter((i) => i !== null).join('_'));
return merge(expr, newExpr, JOINER);
}, subExpr),
);
} else {
subExpr = generateFilterExpression(value, [prefix, name].filter((val) => val !== null).join('_'));
}
break;
}
case 'not': {
subExpr = scopeExpression(generateFilterExpression(value, [prefix, name].filter((val) => val !== null).join('_')));
subExpr.expressions.unshift('NOT');
break;
}
case 'between': {
const expr1 = createExpressionValueName(parent, 'between_1', prefix);
const expr2 = createExpressionValueName(parent, 'between_2', prefix);
const exprName = createExpressionName(parent);
const subExprExpr = `${createExpressionFieldName(parent)} BETWEEN ${expr1} AND ${expr2}`;
const exprValues = {
...createExpressionValue(parent, 'between_1', value[0], prefix),
...createExpressionValue(parent, 'between_2', value[1], prefix),
};
subExpr = {
expressions: [subExprExpr],
expressionNames: exprName,
expressionValues: exprValues,
};
break;
}
case 'ne':
case 'eq':
case 'gt':
case 'ge':
case 'lt':
case 'le': {
const operator = OPERATOR_MAP[name];
subExpr = {
expressions: [`(${fieldName} ${operator} ${filedValueName})`],
expressionNames: createExpressionName(parent),
expressionValues: createExpressionValue(parent, name, value, prefix),
};
break;
}
case 'attributeExists': {
const existsName = value === true ? 'attribute_exists' : 'attribute_not_exists';
subExpr = {
expressions: [`(${existsName}(${fieldName}))`],
expressionNames: createExpressionName(parent),
expressionValues: [],
};
break;
}
case 'contains':
case 'notContains':
case 'beginsWith': {
const functionName = FUNCTION_MAP[name];
subExpr = {
expressions: [`(${functionName}(${fieldName}, ${filedValueName}))`],
expressionNames: createExpressionName(parent),
expressionValues: createExpressionValue(parent, name, value, prefix),
};
break;
}
case 'in': {
const operatorName = OPERATOR_MAP[name];
subExpr = {
expressions: [`(${operatorName}(${filedValueName}, ${fieldName}))`],
expressionNames: createExpressionName(parent),
expressionValues: createExpressionValue(parent, name, value, prefix),
};
break;
}
default:
subExpr = scopeExpression(generateFilterExpression(value, prefix, name));
}
return merge(sum, subExpr);
},
{
expressions: [],
expressionNames: {},
expressionValues: {},
},
);

return expr;
}

function merge(expr1, expr2, joinCondition = 'AND') {
if (!expr2.expressions.length) {
return expr1;
}

const res = {
expressions: [...expr1.expressions, expr1.expressions.length ? joinCondition : '', ...expr2.expressions],
expressionNames: { ...expr1.expressionNames, ...expr2.expressionNames },
expressionValues: { ...expr1.expressionValues, ...expr2.expressionValues },
};
return res;
}

function createExpressionValueName(fieldName, op, prefix) {
return `:${[prefix, fieldName, op].filter((name) => name).join('_')}`;
}
function createExpressionName(fieldName) {
return {
[createExpressionFieldName(fieldName)]: fieldName,
};
}

function createExpressionFieldName(fieldName) {
return `#${fieldName}`;
}
function createExpressionValue(fieldName, op, value, prefix) {
const exprName = createExpressionValueName(fieldName, op, prefix);
const exprValue = dynamodbUtils.toDynamoDB(value);
return {
[`${exprName}`]: exprValue,
};
}

function scopeExpression(expr) {
const result = { ...expr };
result.expressions = result.expressions.filter((e) => !!e);
if (result.expressions.length > 1) {
result.expressions = ['(' + result.expressions.join(' ') + ')'];
}
return result;
}
34 changes: 34 additions & 0 deletions test_in_docker.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/env bash

set -euo pipefail

# Run as an entrypoint to docker to install the current package, and test that
# node can import it as per the LocalStack AppSync emulation.
# This script both runs the test, and acts as its own entrypoint

if [ -z ${TEST_IN_DOCKER_ENTRYPOINT:-} ]; then
# test script
echo Test script $0
script_path=$(readlink -f $0)
project_root=$(dirname $script_path)
docker run \
--rm \
-v $project_root:/src \
-v $script_path:/test_in_docker.sh:ro \
--workdir /test \
--entrypoint bash \
-e TEST_IN_DOCKER_ENTRYPOINT=1 \
${TEST_IMAGE_NAME:-public.ecr.aws/lambda/nodejs:16} /test_in_docker.sh
else
# entrypoint
echo Entrypoint
echo '{"dependencies": {"@aws-appsync/utils":"/src"}}' > package.json
npm install

echo "import { util } from '@aws-appsync/utils';" > main.mjs
echo "console.log('id: ', util.autoId());" >> main.mjs
echo "console.log('toDynamoDB: ', util.dynamodb.toDynamoDB('test'));" >> main.mjs

echo "Checking package:"
node main.mjs
fi
Loading