Merge pull request #15664 from Budibase/demock-dynamodb
De-mock dynamodb
This commit is contained in:
commit
2218463269
|
@ -165,6 +165,7 @@ jobs:
|
||||||
oracle,
|
oracle,
|
||||||
sqs,
|
sqs,
|
||||||
elasticsearch,
|
elasticsearch,
|
||||||
|
dynamodb,
|
||||||
none,
|
none,
|
||||||
]
|
]
|
||||||
steps:
|
steps:
|
||||||
|
@ -205,6 +206,8 @@ jobs:
|
||||||
docker pull postgres:9.5.25
|
docker pull postgres:9.5.25
|
||||||
elif [ "${{ matrix.datasource }}" == "elasticsearch" ]; then
|
elif [ "${{ matrix.datasource }}" == "elasticsearch" ]; then
|
||||||
docker pull elasticsearch@${{ steps.dotenv.outputs.ELASTICSEARCH_SHA }}
|
docker pull elasticsearch@${{ steps.dotenv.outputs.ELASTICSEARCH_SHA }}
|
||||||
|
elif [ "${{ matrix.datasource }}" == "dynamodb" ]; then
|
||||||
|
docker pull amazon/dynamodb-local@${{ steps.dotenv.outputs.DYNAMODB_SHA }}
|
||||||
fi
|
fi
|
||||||
docker pull minio/minio &
|
docker pull minio/minio &
|
||||||
docker pull redis &
|
docker pull redis &
|
||||||
|
|
|
@ -3,4 +3,5 @@ MYSQL_SHA=sha256:9de9d54fecee6253130e65154b930978b1fcc336bcc86dfd06e89b72a2588eb
|
||||||
POSTGRES_SHA=sha256:bd0d8e485d1aca439d39e5ea99b931160bd28d862e74c786f7508e9d0053090e
|
POSTGRES_SHA=sha256:bd0d8e485d1aca439d39e5ea99b931160bd28d862e74c786f7508e9d0053090e
|
||||||
MONGODB_SHA=sha256:afa36bca12295b5f9dae68a493c706113922bdab520e901bd5d6c9d7247a1d8d
|
MONGODB_SHA=sha256:afa36bca12295b5f9dae68a493c706113922bdab520e901bd5d6c9d7247a1d8d
|
||||||
MARIADB_SHA=sha256:e59ba8783bf7bc02a4779f103bb0d8751ac0e10f9471089709608377eded7aa8
|
MARIADB_SHA=sha256:e59ba8783bf7bc02a4779f103bb0d8751ac0e10f9471089709608377eded7aa8
|
||||||
ELASTICSEARCH_SHA=sha256:9a6443f55243f6acbfeb4a112d15eb3b9aac74bf25e0e39fa19b3ddd3a6879d0
|
ELASTICSEARCH_SHA=sha256:9a6443f55243f6acbfeb4a112d15eb3b9aac74bf25e0e39fa19b3ddd3a6879d0
|
||||||
|
DYNAMODB_SHA=sha256:cf8cebd061f988628c02daff10fdb950a54478feff9c52f6ddf84710fe3c3906
|
|
@ -14,15 +14,14 @@ import {
|
||||||
UpdateCommandInput,
|
UpdateCommandInput,
|
||||||
DeleteCommandInput,
|
DeleteCommandInput,
|
||||||
} from "@aws-sdk/lib-dynamodb"
|
} from "@aws-sdk/lib-dynamodb"
|
||||||
import { DynamoDB } from "@aws-sdk/client-dynamodb"
|
import { DynamoDB, DynamoDBClientConfig } from "@aws-sdk/client-dynamodb"
|
||||||
import { AWS_REGION } from "../constants"
|
import { AWS_REGION } from "../constants"
|
||||||
|
|
||||||
interface DynamoDBConfig {
|
export interface DynamoDBConfig {
|
||||||
region: string
|
region: string
|
||||||
accessKeyId: string
|
accessKeyId: string
|
||||||
secretAccessKey: string
|
secretAccessKey: string
|
||||||
endpoint?: string
|
endpoint?: string
|
||||||
currentClockSkew?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const SCHEMA: Integration = {
|
const SCHEMA: Integration = {
|
||||||
|
@ -138,22 +137,16 @@ const SCHEMA: Integration = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
class DynamoDBIntegration implements IntegrationBase {
|
export class DynamoDBIntegration implements IntegrationBase {
|
||||||
private config: DynamoDBConfig
|
private config: DynamoDBClientConfig
|
||||||
private client
|
private client: DynamoDBDocument
|
||||||
|
|
||||||
constructor(config: DynamoDBConfig) {
|
constructor(config: DynamoDBConfig) {
|
||||||
this.config = config
|
|
||||||
|
|
||||||
// User is using a local dynamoDB endpoint, don't auth with remote
|
|
||||||
if (this.config?.endpoint?.includes("localhost")) {
|
|
||||||
// @ts-ignore
|
|
||||||
this.config = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.config = {
|
this.config = {
|
||||||
...this.config,
|
credentials: {
|
||||||
currentClockSkew: true,
|
accessKeyId: config.accessKeyId,
|
||||||
|
secretAccessKey: config.secretAccessKey,
|
||||||
|
},
|
||||||
region: config.region || AWS_REGION,
|
region: config.region || AWS_REGION,
|
||||||
endpoint: config.endpoint || undefined,
|
endpoint: config.endpoint || undefined,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,167 +1,108 @@
|
||||||
jest.mock("@aws-sdk/lib-dynamodb", () => ({
|
import { Datasource } from "@budibase/types"
|
||||||
DynamoDBDocument: {
|
import { DynamoDBConfig, DynamoDBIntegration } from "../dynamodb"
|
||||||
from: jest.fn(() => ({
|
import { DatabaseName, datasourceDescribe } from "./utils"
|
||||||
update: jest.fn(),
|
import {
|
||||||
put: jest.fn(),
|
CreateTableCommandInput,
|
||||||
query: jest.fn(() => ({
|
DynamoDB,
|
||||||
Items: [],
|
DynamoDBClientConfig,
|
||||||
})),
|
} from "@aws-sdk/client-dynamodb"
|
||||||
scan: jest.fn(() => ({
|
|
||||||
Items: [],
|
|
||||||
})),
|
|
||||||
delete: jest.fn(),
|
|
||||||
get: jest.fn(),
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
jest.mock("@aws-sdk/client-dynamodb")
|
|
||||||
import { default as DynamoDBIntegration } from "../dynamodb"
|
|
||||||
|
|
||||||
class TestConfiguration {
|
const describes = datasourceDescribe({ only: [DatabaseName.DYNAMODB] })
|
||||||
integration: any
|
|
||||||
|
|
||||||
constructor(config: any = {}) {
|
async function createTable(client: DynamoDB, req: CreateTableCommandInput) {
|
||||||
this.integration = new DynamoDBIntegration.integration(config)
|
try {
|
||||||
|
await client.deleteTable({ TableName: req.TableName })
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.name !== "ResourceNotFoundException") {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return await client.createTable(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("DynamoDB Integration", () => {
|
if (describes.length > 0) {
|
||||||
let config: any
|
describe.each(describes)("DynamoDB Integration", ({ dsProvider }) => {
|
||||||
let tableName = "Users"
|
let table = "Users"
|
||||||
|
let rawDatasource: Datasource
|
||||||
|
let dynamodb: DynamoDBIntegration
|
||||||
|
|
||||||
beforeEach(() => {
|
function item(json: Record<string, any>) {
|
||||||
config = new TestConfiguration()
|
return { table, json: { Item: json } }
|
||||||
})
|
|
||||||
|
|
||||||
it("calls the create method with the correct params", async () => {
|
|
||||||
await config.integration.create({
|
|
||||||
table: tableName,
|
|
||||||
json: {
|
|
||||||
Name: "John",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
expect(config.integration.client.put).toHaveBeenCalledWith({
|
|
||||||
TableName: tableName,
|
|
||||||
Name: "John",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it("calls the read method with the correct params", async () => {
|
|
||||||
const indexName = "Test"
|
|
||||||
|
|
||||||
const response = await config.integration.read({
|
|
||||||
table: tableName,
|
|
||||||
index: indexName,
|
|
||||||
json: {},
|
|
||||||
})
|
|
||||||
expect(config.integration.client.query).toHaveBeenCalledWith({
|
|
||||||
TableName: tableName,
|
|
||||||
IndexName: indexName,
|
|
||||||
})
|
|
||||||
expect(response).toEqual([])
|
|
||||||
})
|
|
||||||
|
|
||||||
it("calls the scan method with the correct params", async () => {
|
|
||||||
const indexName = "Test"
|
|
||||||
|
|
||||||
const response = await config.integration.scan({
|
|
||||||
table: tableName,
|
|
||||||
index: indexName,
|
|
||||||
json: {},
|
|
||||||
})
|
|
||||||
expect(config.integration.client.scan).toHaveBeenCalledWith({
|
|
||||||
TableName: tableName,
|
|
||||||
IndexName: indexName,
|
|
||||||
})
|
|
||||||
expect(response).toEqual([])
|
|
||||||
})
|
|
||||||
|
|
||||||
it("calls the get method with the correct params", async () => {
|
|
||||||
await config.integration.get({
|
|
||||||
table: tableName,
|
|
||||||
json: {
|
|
||||||
Id: 123,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(config.integration.client.get).toHaveBeenCalledWith({
|
|
||||||
TableName: tableName,
|
|
||||||
Id: 123,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it("calls the update method with the correct params", async () => {
|
|
||||||
await config.integration.update({
|
|
||||||
table: tableName,
|
|
||||||
json: {
|
|
||||||
Name: "John",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
expect(config.integration.client.update).toHaveBeenCalledWith({
|
|
||||||
TableName: tableName,
|
|
||||||
Name: "John",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it("calls the delete method with the correct params", async () => {
|
|
||||||
await config.integration.delete({
|
|
||||||
table: tableName,
|
|
||||||
json: {
|
|
||||||
Name: "John",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
expect(config.integration.client.delete).toHaveBeenCalledWith({
|
|
||||||
TableName: tableName,
|
|
||||||
Name: "John",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it("configures the dynamoDB constructor based on an empty endpoint parameter", async () => {
|
|
||||||
const config = {
|
|
||||||
region: "us-east-1",
|
|
||||||
accessKeyId: "test",
|
|
||||||
secretAccessKey: "test",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const integration: any = new DynamoDBIntegration.integration(config)
|
function key(json: Record<string, any>) {
|
||||||
|
return { table, json: { Key: json } }
|
||||||
expect(integration.config).toEqual({
|
|
||||||
currentClockSkew: true,
|
|
||||||
...config,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it("configures the dynamoDB constructor based on a localhost endpoint parameter", async () => {
|
|
||||||
const config = {
|
|
||||||
region: "us-east-1",
|
|
||||||
accessKeyId: "test",
|
|
||||||
secretAccessKey: "test",
|
|
||||||
endpoint: "localhost:8080",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const integration: any = new DynamoDBIntegration.integration(config)
|
beforeEach(async () => {
|
||||||
|
const ds = await dsProvider()
|
||||||
|
rawDatasource = ds.rawDatasource!
|
||||||
|
dynamodb = new DynamoDBIntegration(
|
||||||
|
rawDatasource.config! as DynamoDBConfig
|
||||||
|
)
|
||||||
|
|
||||||
expect(integration.config).toEqual({
|
const config: DynamoDBClientConfig = {
|
||||||
region: "us-east-1",
|
credentials: {
|
||||||
currentClockSkew: true,
|
accessKeyId: "test",
|
||||||
endpoint: "localhost:8080",
|
secretAccessKey: "test",
|
||||||
|
},
|
||||||
|
region: "us-east-1",
|
||||||
|
endpoint: rawDatasource.config!.endpoint,
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new DynamoDB(config)
|
||||||
|
await createTable(client, {
|
||||||
|
TableName: table,
|
||||||
|
KeySchema: [{ AttributeName: "Id", KeyType: "HASH" }],
|
||||||
|
AttributeDefinitions: [{ AttributeName: "Id", AttributeType: "N" }],
|
||||||
|
ProvisionedThroughput: { ReadCapacityUnits: 1, WriteCapacityUnits: 1 },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("can create and read a record", async () => {
|
||||||
|
await dynamodb.create(item({ Id: 1, Name: "John" }))
|
||||||
|
|
||||||
|
const resp = await dynamodb.get(key({ Id: 1 }))
|
||||||
|
expect(resp.Item).toEqual({ Id: 1, Name: "John" })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("can scan", async () => {
|
||||||
|
await dynamodb.create(item({ Id: 1, Name: "John" }))
|
||||||
|
await dynamodb.create(item({ Id: 2, Name: "Jane" }))
|
||||||
|
await dynamodb.create(item({ Id: 3, Name: "Jack" }))
|
||||||
|
|
||||||
|
const resp = await dynamodb.scan({ table, json: {}, index: null })
|
||||||
|
expect(resp).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{ Id: 1, Name: "John" },
|
||||||
|
{ Id: 2, Name: "Jane" },
|
||||||
|
{ Id: 3, Name: "Jack" },
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("can update", async () => {
|
||||||
|
await dynamodb.create(item({ Id: 1, Foo: "John" }))
|
||||||
|
await dynamodb.update({
|
||||||
|
table,
|
||||||
|
json: {
|
||||||
|
Key: { Id: 1 },
|
||||||
|
UpdateExpression: "SET Foo = :foo",
|
||||||
|
ExpressionAttributeValues: { ":foo": "Jane" },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const updatedRecord = await dynamodb.get(key({ Id: 1 }))
|
||||||
|
expect(updatedRecord.Item).toEqual({ Id: 1, Foo: "Jane" })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("can delete", async () => {
|
||||||
|
await dynamodb.create(item({ Id: 1, Name: "John" }))
|
||||||
|
await dynamodb.delete(key({ Id: 1 }))
|
||||||
|
|
||||||
|
const deletedRecord = await dynamodb.get(key({ Id: 1 }))
|
||||||
|
expect(deletedRecord.Item).toBeUndefined()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
}
|
||||||
it("configures the dynamoDB constructor based on a remote endpoint parameter", async () => {
|
|
||||||
const config = {
|
|
||||||
region: "us-east-1",
|
|
||||||
accessKeyId: "test",
|
|
||||||
secretAccessKey: "test",
|
|
||||||
endpoint: "dynamodb.aws.foo.net",
|
|
||||||
}
|
|
||||||
|
|
||||||
const integration = new DynamoDBIntegration.integration(config)
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
expect(integration.config).toEqual({
|
|
||||||
currentClockSkew: true,
|
|
||||||
...config,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { Datasource, SourceName } from "@budibase/types"
|
||||||
|
import { GenericContainer, Wait } from "testcontainers"
|
||||||
|
import { testContainerUtils } from "@budibase/backend-core/tests"
|
||||||
|
import { startContainer } from "."
|
||||||
|
import { DYNAMODB_IMAGE } from "./images"
|
||||||
|
import { DynamoDBConfig } from "../../dynamodb"
|
||||||
|
|
||||||
|
let ports: Promise<testContainerUtils.Port[]>
|
||||||
|
|
||||||
|
export async function getDatasource(): Promise<Datasource> {
|
||||||
|
if (!ports) {
|
||||||
|
ports = startContainer(
|
||||||
|
new GenericContainer(DYNAMODB_IMAGE)
|
||||||
|
.withExposedPorts(8000)
|
||||||
|
.withWaitStrategy(
|
||||||
|
Wait.forSuccessfulCommand(
|
||||||
|
// https://stackoverflow.com/a/77373799
|
||||||
|
`if [ "$(curl -s -o /dev/null -I -w ''%{http_code}'' http://localhost:8000)" == "400" ]; then exit 0; else exit 1; fi`
|
||||||
|
).withStartupTimeout(60000)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const port = (await ports).find(x => x.container === 8000)?.host
|
||||||
|
if (!port) {
|
||||||
|
throw new Error("DynamoDB port not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
const config: DynamoDBConfig = {
|
||||||
|
accessKeyId: "test",
|
||||||
|
secretAccessKey: "test",
|
||||||
|
region: "us-east-1",
|
||||||
|
endpoint: `http://127.0.0.1:${port}`,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "datasource",
|
||||||
|
source: SourceName.DYNAMODB,
|
||||||
|
config,
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,3 +13,4 @@ export const POSTGRES_LEGACY_IMAGE = `postgres:9.5.25`
|
||||||
export const MONGODB_IMAGE = `mongo@${process.env.MONGODB_SHA}`
|
export const MONGODB_IMAGE = `mongo@${process.env.MONGODB_SHA}`
|
||||||
export const MARIADB_IMAGE = `mariadb@${process.env.MARIADB_SHA}`
|
export const MARIADB_IMAGE = `mariadb@${process.env.MARIADB_SHA}`
|
||||||
export const ELASTICSEARCH_IMAGE = `elasticsearch@${process.env.ELASTICSEARCH_SHA}`
|
export const ELASTICSEARCH_IMAGE = `elasticsearch@${process.env.ELASTICSEARCH_SHA}`
|
||||||
|
export const DYNAMODB_IMAGE = `amazon/dynamodb-local@${process.env.DYNAMODB_SHA}`
|
||||||
|
|
|
@ -7,6 +7,7 @@ import * as mssql from "./mssql"
|
||||||
import * as mariadb from "./mariadb"
|
import * as mariadb from "./mariadb"
|
||||||
import * as oracle from "./oracle"
|
import * as oracle from "./oracle"
|
||||||
import * as elasticsearch from "./elasticsearch"
|
import * as elasticsearch from "./elasticsearch"
|
||||||
|
import * as dynamodb from "./dynamodb"
|
||||||
import { testContainerUtils } from "@budibase/backend-core/tests"
|
import { testContainerUtils } from "@budibase/backend-core/tests"
|
||||||
import { Knex } from "knex"
|
import { Knex } from "knex"
|
||||||
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||||
|
@ -25,6 +26,7 @@ export enum DatabaseName {
|
||||||
ORACLE = "oracle",
|
ORACLE = "oracle",
|
||||||
SQS = "sqs",
|
SQS = "sqs",
|
||||||
ELASTICSEARCH = "elasticsearch",
|
ELASTICSEARCH = "elasticsearch",
|
||||||
|
DYNAMODB = "dynamodb",
|
||||||
}
|
}
|
||||||
|
|
||||||
const DATASOURCE_PLUS = [
|
const DATASOURCE_PLUS = [
|
||||||
|
@ -50,6 +52,7 @@ const providers: Record<DatabaseName, DatasourceProvider> = {
|
||||||
// rest
|
// rest
|
||||||
[DatabaseName.ELASTICSEARCH]: elasticsearch.getDatasource,
|
[DatabaseName.ELASTICSEARCH]: elasticsearch.getDatasource,
|
||||||
[DatabaseName.MONGODB]: mongodb.getDatasource,
|
[DatabaseName.MONGODB]: mongodb.getDatasource,
|
||||||
|
[DatabaseName.DYNAMODB]: dynamodb.getDatasource,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DatasourceDescribeReturnPromise {
|
export interface DatasourceDescribeReturnPromise {
|
||||||
|
|
|
@ -12,8 +12,7 @@ nock.enableNetConnect(host => {
|
||||||
return (
|
return (
|
||||||
host.includes("localhost") ||
|
host.includes("localhost") ||
|
||||||
host.includes("127.0.0.1") ||
|
host.includes("127.0.0.1") ||
|
||||||
host.includes("::1") ||
|
host.includes("::1")
|
||||||
host.includes("ethereal.email") // used in realEmail.spec.ts
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue