import { batchReadAll } from "@webiny/db-dynamodb/utils/batchRead" ; import { batchWriteAll } from "@webiny/db-dynamodb/utils/batchWrite" ; import { cleanupItem , cleanupItems } from "@webiny/db-dynamodb/utils/cleanup" ; import { queryAll } from "@webiny/db-dynamodb/utils/query" ; import Error from "@webiny/error" ; import { createTable } from "~/definitions/table" ; import { createTenantEntity } from "~/definitions/tenantEntity" ; import { createSystemEntity } from "~/definitions/systemEntity" ; import { createDomainEntity } from "~/definitions/domainEntity" ; import { CreateTenancyStorageOperations , ENTITIES } from "~/types" ; import { System , Tenant , TenantDomain } from "@webiny/api-tenancy/types" ; const reservedFields = [ "PK" , "SK" , "index" , "data" ] ; const isReserved = name => { if ( reservedFields . includes ( name ) ) { throw new Error ( `Attribute name "${ name }" is not allowed.` , "ATTRIBUTE_NOT_ALLOWED" , { name } ) ; } } ; export const createStorageOperations = params => { const { table , documentClient , attributes = { } } = params ; if ( attributes ) { Object . values ( attributes ) . forEach ( attrs => { Object . keys ( attrs ) . forEach ( isReserved ) ; } ) ; } const tableInstance = createTable ( { table , documentClient } ) ; const entities = { tenants : createTenantEntity ( { entityName : ENTITIES . TENANT , table : tableInstance , attributes : attributes [ ENTITIES . TENANT ] } ) , domains : createDomainEntity ( { entityName : ENTITIES . DOMAIN , table : tableInstance , attributes : attributes [ ENTITIES . DOMAIN ] } ) , system : createSystemEntity ( { entityName : ENTITIES . SYSTEM , table : tableInstance , attributes : attributes [ ENTITIES . SYSTEM ] } ) } ; const systemKeys = { PK : "T#root#SYSTEM" , SK : "TENANCY" } ; const createNewDomainsRecords = ( tenant , existingDomains = [ ] ) => { return tenant . settings . domains . map ( ( { fqdn } ) => { if ( existingDomains . find ( d => d . fqdn === fqdn ) ) { return ; } return { PK : `DOMAIN#${ fqdn }` , SK : `A` , GSI1_PK : `DOMAINS` , GSI1_SK : `T#${ tenant . id }#${ fqdn }` , tenant : tenant . id , fqdn , webinyVersion : tenant . webinyVersion } ; } ) . filter ( Boolean ) ; } ; return { getTable ( ) { return tableInstance ; } , getEntities ( ) { return entities ; } , async createSystemData ( data ) { try { await entities . system . put ( { ... systemKeys , ... data } ) ; return data ; } catch ( err ) { throw Error . from ( err , { message : "Could not create system record." , code : "CREATE_SYSTEM_ERROR" , data : { keys : systemKeys , data } } ) ; } } , async getSystemData ( ) { try { const result = await entities . system . get ( systemKeys ) ; if ( ! result || ! result . Item ) { return null ; } return cleanupItem ( entities . system , result . Item ) ; } catch ( err ) { throw Error . from ( err , { message : "Could not load system record." , code : "GET_SYSTEM_ERROR" , data : { keys : systemKeys } } ) ; } } , async updateSystemData ( data ) { try { await entities . system . put ( { ... systemKeys , ... data } ) ; return data ; } catch ( err ) { throw Error . from ( err , { message : "Could not update system record." , code : "UPDATE_SYSTEM_ERROR" , data : { keys : systemKeys , data } } ) ; } } , async getTenantsByIds < TTenant extends Tenant = Tenant > ( ids ) { const items = ids . map ( id => entities . tenants . getBatch ( { PK : `T#${ id }` , SK : "A" } ) ) ; const tenants = await batchReadAll < TTenant > ( { table : tableInstance , items } ) ; return cleanupItems ( entities . tenants , tenants ) ; } , async listTenants < TTenant extends Tenant = Tenant > ( { parent } ) { const tenants = await queryAll < TTenant > ( { entity : entities . tenants , partitionKey : `T#${ parent }` , options : { index : "GSI1" , beginsWith : "T#" } } ) ; return cleanupItems ( entities . tenants , tenants ) ; } , async createTenant < TTenant extends Tenant = Tenant > ( data ) { const keys = { PK : `T#${ data . id }` , SK : "A" , GSI1_PK : data . parent ? `T#${ data . parent }` : undefined , GSI1_SK : data . parent ? `T#${ data . id }` : undefined } ; try { const items = [ entities . tenants . putBatch ( { ... keys , ... data } ) ] ; const newDomains = createNewDomainsRecords ( data ) ; newDomains . forEach ( record => { items . push ( entities . domains . putBatch ( record ) ) ; } ) ; await batchWriteAll ( { table : tableInstance , items : items } ) ; return data as TTenant ; } catch ( err ) { throw Error . from ( err , { message : "Could not create tenant record." , code : "CREATE_TENANT_ERROR" , data : { keys , data } } ) ; } } , async updateTenant < TTenant extends Tenant = Tenant > ( data ) { const tenantPK = `T#${ data . id }` ; const keys = { PK : tenantPK , SK : "A" , GSI1_PK : data . parent ? `T#${ data . parent }` : undefined , GSI1_SK : data . parent ? `T#${ data . id }` : undefined } ; const items = [ entities . tenants . putBatch ( { ... keys , ... data } ) ] ; const existingDomains = await queryAll < TenantDomain > ( { entity : entities . domains , partitionKey : "DOMAINS" , options : { index : "GSI1" , beginsWith : `T#${ data . id }` } } ) ; const newDomains = createNewDomainsRecords ( data , existingDomains ) ; const deleteDomains = [ ] ; for ( const { fqdn } of existingDomains ) { if ( ! data . settings . domains . find ( d => d . fqdn === fqdn ) ) { deleteDomains . push ( { PK : `DOMAIN#${ fqdn }` , SK : `A` } ) ; } } try { newDomains . forEach ( record => { items . push ( entities . domains . putBatch ( record ) ) ; } ) ; deleteDomains . forEach ( item => { items . push ( entities . domains . deleteBatch ( item ) ) ; } ) ; await batchWriteAll ( { table : tableInstance , items : items } ) ; return data as TTenant ; } catch ( err ) { throw Error . from ( err , { message : "Could not update tenant record." , code : "CREATE_TENANT_ERROR" , data : { keys , data } } ) ; } } , async deleteTenant ( id ) { await entities . tenants . delete ( { PK : `T#${ id }` , SK : "A" } ) ; } } ; } ;