Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Azure provider MVP #258

Merged
merged 29 commits into from
Jul 20, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
e198469
[BOOST-652]
rdoria1 May 20, 2020
941552e
[BOOST-652]
rdoria1 May 20, 2020
7970d0e
[BOOST-652]
rdoria1 Jun 1, 2020
0a2ac35
Merge branch 'BOOST-652-skeleton-azure-infra' of github.com:theam/boo…
MarcAstr0 Jun 5, 2020
9a41a39
Added more Azure resources
MarcAstr0 Jun 11, 2020
95bcf56
More Azure resources for REST API
MarcAstr0 Jun 19, 2020
96efa15
Azure adapters for API and GraphQL
MarcAstr0 Jun 22, 2020
af9202d
Cosmos DB added to infrastructure
MarcAstr0 Jun 28, 2020
ef4c502
Implemented events adapter
MarcAstr0 Jun 30, 2020
2fdc897
Implemented read model and searcher adapter
MarcAstr0 Jul 8, 2020
7c4dbf2
Unit tests for Azure provider
MarcAstr0 Jul 8, 2020
6589972
Refactored Azure provider infrastructure
MarcAstr0 Jul 8, 2020
a97970b
Some fixes to the events and read model adapters
MarcAstr0 Jul 9, 2020
b89e0df
Merge branch 'master-mirror' into BOOST-730-implement-azure-resources
MarcAstr0 Jul 9, 2020
700c306
Several fixes after merging with master-mirror
MarcAstr0 Jul 10, 2020
907ffa7
Fixed yarn.lock
MarcAstr0 Jul 14, 2020
4a7b39b
Cleaned up utils.ts in the Azure infra package
MarcAstr0 Jul 14, 2020
e9d66c5
Merge branch 'master-mirror' into BOOST-730-implement-azure-resources
MarcAstr0 Jul 14, 2020
9339fa4
Added @ts-ignore to conflicting lines
MarcAstr0 Jul 14, 2020
e3ec4fa
Set template paths as class-level constants
MarcAstr0 Jul 14, 2020
8ad46bc
Refactored template loading in Azure infra
MarcAstr0 Jul 14, 2020
89062e0
Fixed debug message placement in searcher-adapter
MarcAstr0 Jul 14, 2020
09e369c
Pass Azure credentials as function parameters
MarcAstr0 Jul 16, 2020
44cf4cf
Small string change in searcher-adapter
MarcAstr0 Jul 16, 2020
edaa1d4
Use path.join in packageAzureFunction method
MarcAstr0 Jul 16, 2020
ba62f26
Refactored contants.ts to resemble AWS provider
MarcAstr0 Jul 16, 2020
df27b67
Removed unnecessary type conversion
MarcAstr0 Jul 16, 2020
13fc284
Simplified returns in the events adapter
MarcAstr0 Jul 16, 2020
3676337
Merge branch 'master-mirror' into BOOST-730-implement-azure-resources
MarcAstr0 Jul 20, 2020
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
Prev Previous commit
Next Next commit
Implemented events adapter
  • Loading branch information
MarcAstr0 committed Jun 30, 2020
commit ef4c502d45ddd8b3039c25cbd7e55491859a1663
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,19 @@ export class ApplicationStackBuilder {
{},
'../templates/storage-account.json'
)
const cosmosDbDeployment = await this.buildResource(
resourceManagementClient,
resourceGroupName,
{
databaseName: { value: this.config.resourceNames.applicationStack },
containerName: { value: this.config.resourceNames.eventsStore },
partitionKey: { value: eventStorePartitionKeyAttribute },
},
'../templates/cosmos-db-account.json'
)

const cosmosDbConnectionString = cosmosDbDeployment.properties?.outputs.connectionString.value

const functionAppDeployment = await this.buildResource(
resourceManagementClient,
resourceGroupName,
Expand Down Expand Up @@ -82,6 +95,8 @@ export class ApplicationStackBuilder {
appSettings.properties.BOOSTER_ENV = this.config.environmentName
// @ts-ignore
appSettings.properties.BOOSTER_REST_API_URL = `https://${apiManagementServiceDeployment.properties?.outputs.apiManagementServiceName.value}.azure-api.net/${this.config.environmentName}`
// @ts-ignore
appSettings.properties.COSMOSDB_CONNECTION_STRING = cosmosDbConnectionString

// update app settings
await webSiteManagementClient.webApps.updateApplicationSettings(
Expand All @@ -92,24 +107,48 @@ export class ApplicationStackBuilder {
}
)

const zipPath = await this.packageAzureFunction('graphql', {
bindings: [
{
authLevel: 'anonymous',
type: 'httpTrigger',
direction: 'in',
name: 'rawRequest',
methods: ['post'],
const zipPath = await this.packageAzureFunction([
{
functionName: 'graphql',
functionConfig: {
bindings: [
{
authLevel: 'anonymous',
type: 'httpTrigger',
direction: 'in',
name: 'rawRequest',
methods: ['post'],
},
{
type: 'http',
direction: 'out',
name: '$return',
},
],
scriptFile: '../dist/index.js',
entryPoint: this.config.serveGraphQLHandler.split('.')[1],
},
{
type: 'http',
direction: 'out',
name: '$return',
},
{
functionName: 'eventHandler',
functionConfig: {
bindings: [
{
type: 'cosmosDBTrigger',
name: 'documents',
direction: 'in',
leaseCollectionName: 'leases',
connectionStringSetting: 'COSMOSDB_CONNECTION_STRING',
databaseName: this.config.resourceNames.applicationStack,
collectionName: this.config.resourceNames.eventsStore,
createLeaseCollectionIfNotExists: 'true',
},
],
scriptFile: '../dist/index.js',
entryPoint: this.config.eventDispatcherHandler.split('.')[1],
},
],
scriptFile: '../dist/index.js',
entryPoint: 'boosterServeGraphQL',
})
},
])

// @ts-ignore
const deployResponse = await this.deployFunctionPackage(
Expand Down Expand Up @@ -169,15 +208,21 @@ export class ApplicationStackBuilder {
)
}

private async packageAzureFunction(functionName: string, functionConfig: object): Promise<any> {
private async packageAzureFunction(
functionDefinitions: Array<{ functionName: string; functionConfig: object }>
): Promise<any> {
const output = fs.createWriteStream(os.tmpdir() + '/example.zip')
const archive = archiver('zip', {
zlib: { level: 9 }, // Sets the compression level.
})

archive.pipe(output)
archive.glob('**/*')
archive.append(JSON.stringify(functionConfig, null, 2), { name: functionName + '/function.json' })
functionDefinitions.forEach((functionDefinition) => {
archive.append(JSON.stringify(functionDefinition.functionConfig, null, 2), {
name: functionDefinition.functionName + '/function.json',
})
})
archive.finalize()

return new Promise((resolve, reject) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@
}
},
"functions": [],
"variables": {},
"variables": {
"resourceId": "[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDbAccountName'))]",
"apiVersion": "[providers('Microsoft.DocumentDB', 'databaseAccounts').apiVersions[0]]"
},
"resources": [
{
"name": "[parameters('cosmosDbAccountName')]",
Expand Down Expand Up @@ -94,5 +97,18 @@
]
}
],
"outputs": {}
"outputs": {
"documentEndpoint": {
"type": "string",
"value": "[reference(variables('resourceId'), variables('apiVersion')).documentEndpoint]"
},
"accountKey": {
"type": "string",
"value": "[listKeys(variables('resourceId'), variables('apiVersion')).primaryMasterKey]"
},
"connectionString": {
"type": "string",
"value": "[concat('AccountEndpoint=https://', parameters('cosmosDbAccountName'), '.documents.azure.com:443/;AccountKey=', listKeys(variables('resourceId'), variables('apiVersion')).primaryMasterKey, ';')]"
}
}
}
1 change: 1 addition & 0 deletions packages/framework-provider-azure/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"url": "git+https://github.com/boostercloud/booster.git"
},
"dependencies": {
"@azure/cosmos": "^3.7.2",
"@azure/functions": "^1.2.2",
"@boostercloud/framework-types": "^0.3.3"
},
Expand Down
1 change: 1 addition & 0 deletions packages/framework-provider-azure/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export const subscriptionsStoreTTLAttribute = 'expirationTime'
export const environmentVarNames = {
restAPIURL: 'BOOSTER_REST_API_URL',
websocketAPIURL: 'BOOSTER_WEBSOCKET_API_URL',
cosmosDbConnectionString: 'COSMOSDB_CONNECTION_STRING',
} as const
24 changes: 20 additions & 4 deletions packages/framework-provider-azure/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,30 @@
import { ProviderInfrastructure, ProviderLibrary } from '@boostercloud/framework-types'
import { requestFailed, requestSucceeded } from './library/api-adapter'
import { rawGraphQLRequestToEnvelope } from './library/graphql-adapter'
import {
rawEventsToEnvelopes,
storeEvents,
readEntityEventsSince,
readEntityLatestSnapshot,
} from './library/events-adapter'
import { CosmosClient } from '@azure/cosmos'
import { environmentVarNames } from './constants'

let cosmosClient
if (process.env[environmentVarNames.cosmosDbConnectionString]) {
// @ts-ignore
cosmosClient = new CosmosClient(process.env[environmentVarNames.cosmosDbConnectionString])
} else {
cosmosClient = undefined
}

export const Provider: ProviderLibrary = {
// ProviderEventsLibrary
events: {
rawToEnvelopes: undefined as any,
store: undefined as any,
forEntitySince: undefined as any,
latestEntitySnapshot: undefined as any,
rawToEnvelopes: rawEventsToEnvelopes,
store: storeEvents.bind(null, cosmosClient),
forEntitySince: readEntityEventsSince.bind(null, cosmosClient),
latestEntitySnapshot: readEntityLatestSnapshot.bind(null, cosmosClient),
},
// ProviderReadModelsLibrary
readModels: {
Expand Down
107 changes: 107 additions & 0 deletions packages/framework-provider-azure/src/library/events-adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { EventEnvelope } from '@boostercloud/framework-types'
import { Container, CosmosClient, Database, ItemResponse } from '@azure/cosmos'
import { BoosterConfig, Logger, UUID } from '@boostercloud/framework-types'
import { eventStorePartitionKeyAttribute, eventStoreSortKeyAttribute } from '../constants'
import { partitionKeyForEvent } from './partition-keys'

// eslint-disable-next-line @typescript-eslint/no-magic-numbers
const originOfTime = new Date(0).toISOString()

export function rawEventsToEnvelopes(rawEvents: Array<any>): Array<EventEnvelope> {
return rawEvents.map(
(rawEvent: any): EventEnvelope => {
return rawEvent as EventEnvelope
}
)
}

export async function readEntityEventsSince(
cosmosDb: CosmosClient,
config: BoosterConfig,
logger: Logger,
entityTypeName: string,
entityID: UUID,
since?: string
): Promise<Array<EventEnvelope>> {
const fromTime = since ? since : originOfTime
const database: Database = cosmosDb.database(config.resourceNames.applicationStack)
const container: Container = database.container(config.resourceNames.eventsStore)
const { resources } = await container.items
.query({
query: `SELECT * FROM c where c.${eventStorePartitionKeyAttribute} = @partitionKey AND c.${eventStoreSortKeyAttribute} > @fromTime ORDER BY c.${eventStoreSortKeyAttribute} DESC`,
parameters: [
{
name: '@partitionKey',
value: partitionKeyForEvent(entityTypeName, entityID),
},
{
name: '@fromTime',
value: fromTime,
},
],
})
.fetchAll()
return resources.map((resource: any) => {
return resource as EventEnvelope
})
}

export async function readEntityLatestSnapshot(
cosmosDb: CosmosClient,
config: BoosterConfig,
logger: Logger,
entityTypeName: string,
entityID: UUID
): Promise<EventEnvelope | null> {
const database: Database = cosmosDb.database(config.resourceNames.applicationStack)
const container: Container = database.container(config.resourceNames.eventsStore)
const { resources } = await container.items
.query({
query: `SELECT * FROM c where c.${eventStorePartitionKeyAttribute} = @partitionKey ORDER BY c.${eventStoreSortKeyAttribute} DESC LIMIT 1`,
parameters: [
{
name: '@partitionKey',
value: partitionKeyForEvent(entityTypeName, entityID, 'snapshot'),
},
],
})
.fetchAll()

const snapshot = resources[0]
if (snapshot) {
logger.debug(
`[EventsAdapter#readEntityLatestSnapshot] Snapshot found for entity ${entityTypeName} with ID ${entityID}:`,
snapshot
)
return snapshot as EventEnvelope
} else {
logger.debug(
`[EventsAdapter#readEntityLatestSnapshot] No snapshot found for entity ${entityTypeName} with ID ${entityID}.`
)
return null
}
}

export async function storeEvents(
cosmosDb: CosmosClient,
eventEnvelopes: Array<EventEnvelope>,
config: BoosterConfig,
logger: Logger
): Promise<void> {
logger.debug('[EventsAdapter#storeEvents] Storing EventEnvelopes with eventEnvelopes:', eventEnvelopes)
const database: Database = cosmosDb.database(config.resourceNames.applicationStack)
const container: Container = database.container(config.resourceNames.eventsStore)
const events: Array<Promise<ItemResponse<any>>> = eventEnvelopes.map((eventEnvelope) => {
return container.items.create({
...eventEnvelope,
[eventStorePartitionKeyAttribute]: partitionKeyForEvent(
eventEnvelope.entityTypeName,
eventEnvelope.entityID,
eventEnvelope.kind
),
[eventStoreSortKeyAttribute]: new Date().toISOString(),
})
})
await Promise.all(events)
logger.debug('[EventsAdapter#storeEvents] EventEnvelope stored')
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { EventEnvelope, UUID } from '@boostercloud/framework-types'

export function partitionKeyForEvent(
entityTypeName: string,
entityID: UUID,
kind: EventEnvelope['kind'] = 'event'
): string {
return `${entityTypeName}-${entityID}-${kind}`
}
Loading