Skip to content

Commit

Permalink
Fix LocalStack importing issue (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
simonrw authored Oct 20, 2023
2 parents 79b2682 + 92b889c commit 9fbeafe
Show file tree
Hide file tree
Showing 4 changed files with 332 additions and 170 deletions.
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

0 comments on commit 9fbeafe

Please sign in to comment.