Back populate no-op migrations on new app and tenant create

This commit is contained in:
Rory Powell 2022-06-13 10:51:29 +01:00
parent 9c671fd706
commit eb0214a231
44 changed files with 459 additions and 278 deletions

View File

@ -1,5 +1,5 @@
import env from "../environment" import env from "../environment"
import * as tenancy from "../tenancy" import tenancy from "../tenancy"
import * as dbUtils from "../db/utils" import * as dbUtils from "../db/utils"
import { Configs } from "../constants" import { Configs } from "../constants"
import { withCache, TTL, CacheKeys } from "../cache/generic" import { withCache, TTL, CacheKeys } from "../cache/generic"

View File

@ -12,6 +12,7 @@ import sessions from "./security/sessions"
import deprovisioning from "./context/deprovision" import deprovisioning from "./context/deprovision"
import auth from "./auth" import auth from "./auth"
import constants from "./constants" import constants from "./constants"
import * as dbConstants from "./db/constants"
// mimic the outer package exports // mimic the outer package exports
import * as db from "./pkg/db" import * as db from "./pkg/db"
@ -20,7 +21,6 @@ import * as utils from "./pkg/utils"
import redis from "./pkg/redis" import redis from "./pkg/redis"
import cache from "./pkg/cache" import cache from "./pkg/cache"
import context from "./pkg/context" import context from "./pkg/context"
const StaticDatabases = db.StaticDatabases
const init = (opts: any = {}) => { const init = (opts: any = {}) => {
db.init(opts.db) db.init(opts.db)
@ -28,8 +28,8 @@ const init = (opts: any = {}) => {
const core = { const core = {
init, init,
StaticDatabases,
db, db,
...dbConstants,
redis, redis,
objectStore, objectStore,
utils, utils,
@ -37,6 +37,7 @@ const core = {
cache, cache,
auth, auth,
constants, constants,
...constants,
migrations, migrations,
env, env,
accounts, accounts,

View File

@ -0,0 +1,40 @@
import {
MigrationType,
MigrationName,
MigrationDefinition,
} from "@budibase/types"
export const DEFINITIONS: MigrationDefinition[] = [
{
type: MigrationType.GLOBAL,
name: MigrationName.USER_EMAIL_VIEW_CASING,
},
{
type: MigrationType.GLOBAL,
name: MigrationName.QUOTAS_1,
},
{
type: MigrationType.APP,
name: MigrationName.APP_URLS,
},
{
type: MigrationType.GLOBAL,
name: MigrationName.DEVELOPER_QUOTA,
},
{
type: MigrationType.GLOBAL,
name: MigrationName.PUBLISHED_APP_QUOTA,
},
{
type: MigrationType.APP,
name: MigrationName.EVENT_APP_BACKFILL,
},
{
type: MigrationType.GLOBAL,
name: MigrationName.EVENT_GLOBAL_BACKFILL,
},
{
type: MigrationType.INSTALLATION,
name: MigrationName.EVENT_INSTALLATION_BACKFILL,
},
]

View File

@ -1,163 +0,0 @@
const { DEFAULT_TENANT_ID } = require("../constants")
const { doWithDB } = require("../db")
const { DocumentTypes, StaticDatabases } = require("../db/constants")
const { getAllApps } = require("../db/utils")
const environment = require("../environment")
const {
doInTenant,
getTenantIds,
getGlobalDBName,
getTenantId,
} = require("../tenancy")
const context = require("../context")
exports.MIGRATION_TYPES = {
GLOBAL: "global", // run once per tenant, recorded in global db, global db is provided as an argument
APP: "app", // run per app, recorded in each app db, app db is provided as an argument
INSTALLATION: "installation", // run once, recorded in global info db, global info db is provided as an argument
}
exports.getMigrationsDoc = async db => {
// get the migrations doc
try {
return await db.get(DocumentTypes.MIGRATIONS)
} catch (err) {
if (err.status && err.status === 404) {
return { _id: DocumentTypes.MIGRATIONS }
} else {
console.error(err)
throw err
}
}
}
exports.runMigration = async (migration, options = {}) => {
const migrationType = migration.type
let tenantId
if (migrationType !== exports.MIGRATION_TYPES.INSTALLATION) {
tenantId = getTenantId()
}
const migrationName = migration.name
const silent = migration.silent
const log = message => {
if (!silent) {
console.log(message)
}
}
// get the db to store the migration in
let dbNames
if (migrationType === exports.MIGRATION_TYPES.GLOBAL) {
dbNames = [getGlobalDBName()]
} else if (migrationType === exports.MIGRATION_TYPES.APP) {
const apps = await getAllApps(migration.opts)
dbNames = apps.map(app => app.appId)
} else if (migrationType === exports.MIGRATION_TYPES.INSTALLATION) {
dbNames = [StaticDatabases.PLATFORM_INFO.name]
} else {
throw new Error(`Unrecognised migration type [${migrationType}]`)
}
const length = dbNames.length
let count = 0
// run the migration against each db
for (const dbName of dbNames) {
count++
const lengthStatement = length > 1 ? `[${count}/${length}]` : ""
await doWithDB(dbName, async db => {
try {
const doc = await exports.getMigrationsDoc(db)
// exit if the migration has been performed already
if (doc[migrationName]) {
if (
options.force &&
options.force[migrationType] &&
options.force[migrationType].includes(migrationName)
) {
log(
`[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Forcing`
)
} else {
// the migration has already been performed
return
}
}
log(
`[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Running ${lengthStatement}`
)
if (migration.preventRetry) {
// eagerly set the completion date
// so that we never run this migration twice even upon failure
doc[migrationName] = Date.now()
const response = await db.put(doc)
doc._rev = response.rev
}
// run the migration with tenant context
if (migrationType === exports.MIGRATION_TYPES.APP) {
await context.doInAppContext(db.name, async () => {
await migration.fn(db)
})
} else {
await migration.fn(db)
}
log(
`[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Complete`
)
// mark as complete
doc[migrationName] = Date.now()
await db.put(doc)
} catch (err) {
console.error(
`[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Error: `,
err
)
throw err
}
})
}
}
exports.runMigrations = async (migrations, options = {}) => {
let tenantIds
if (environment.MULTI_TENANCY) {
if (!options.tenantIds || !options.tenantIds.length) {
// run for all tenants
tenantIds = await getTenantIds()
} else {
tenantIds = options.tenantIds
}
} else {
// single tenancy
tenantIds = [DEFAULT_TENANT_ID]
}
if (tenantIds.length > 1) {
console.log(`Checking migrations for ${tenantIds.length} tenants`)
} else {
console.log("Checking migrations")
}
let count = 0
// for all tenants
for (const tenantId of tenantIds) {
count++
if (tenantIds.length > 1) {
console.log(`Progress [${count}/${tenantIds.length}]`)
}
// for all migrations
for (const migration of migrations) {
// run the migration
await doInTenant(tenantId, () => exports.runMigration(migration, options))
}
}
console.log("Migrations complete")
}

View File

@ -0,0 +1,2 @@
export * from "./migrations"
export * from "./definitions"

View File

@ -0,0 +1,189 @@
import { DEFAULT_TENANT_ID } from "../constants"
import { doWithDB } from "../db"
import { DocumentTypes, StaticDatabases } from "../db/constants"
import { getAllApps } from "../db/utils"
import environment from "../environment"
import {
doInTenant,
getTenantIds,
getGlobalDBName,
getTenantId,
} from "../tenancy"
import context from "../context"
import { DEFINITIONS } from "."
import {
Migration,
MigrationOptions,
MigrationType,
MigrationNoOpOptions,
} from "@budibase/types"
export const getMigrationsDoc = async (db: any) => {
// get the migrations doc
try {
return await db.get(DocumentTypes.MIGRATIONS)
} catch (err: any) {
if (err.status && err.status === 404) {
return { _id: DocumentTypes.MIGRATIONS }
} else {
console.error(err)
throw err
}
}
}
export const backPopulateMigrations = async (opts: MigrationNoOpOptions) => {
// filter migrations to the type and populate a no-op migration
const migrations: Migration[] = DEFINITIONS.filter(
def => def.type === opts.type
).map(d => ({ ...d, fn: () => {} }))
await runMigrations(migrations, { noOp: opts })
}
export const runMigration = async (
migration: Migration,
options: MigrationOptions = {}
) => {
const migrationType = migration.type
let tenantId: string
if (migrationType !== MigrationType.INSTALLATION) {
tenantId = getTenantId()
}
const migrationName = migration.name
const silent = migration.silent
const log = (message: string) => {
if (!silent) {
console.log(message)
}
}
// get the db to store the migration in
let dbNames
if (migrationType === MigrationType.GLOBAL) {
dbNames = [getGlobalDBName()]
} else if (migrationType === MigrationType.APP) {
if (options.noOp) {
dbNames = [options.noOp.appId]
} else {
const apps = await getAllApps(migration.appOpts)
dbNames = apps.map(app => app.appId)
}
} else if (migrationType === MigrationType.INSTALLATION) {
dbNames = [StaticDatabases.PLATFORM_INFO.name]
} else {
throw new Error(`Unrecognised migration type [${migrationType}]`)
}
const length = dbNames.length
let count = 0
// run the migration against each db
for (const dbName of dbNames) {
count++
const lengthStatement = length > 1 ? `[${count}/${length}]` : ""
await doWithDB(dbName, async (db: any) => {
try {
const doc = await exports.getMigrationsDoc(db)
// the migration has already been run
if (doc[migrationName]) {
// check for force
if (
options.force &&
options.force[migrationType] &&
options.force[migrationType].includes(migrationName)
) {
log(
`[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Forcing`
)
} else {
// no force, exit
return
}
}
// check if the migration is not a no-op
if (!options.noOp) {
log(
`[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Running ${lengthStatement}`
)
if (migration.preventRetry) {
// eagerly set the completion date
// so that we never run this migration twice even upon failure
doc[migrationName] = Date.now()
const response = await db.put(doc)
doc._rev = response.rev
}
// run the migration
if (migrationType === MigrationType.APP) {
await context.doInAppContext(db.name, async () => {
await migration.fn(db)
})
} else {
await migration.fn(db)
}
log(
`[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Complete`
)
}
// mark as complete
doc[migrationName] = Date.now()
await db.put(doc)
} catch (err) {
console.error(
`[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Error: `,
err
)
throw err
}
})
}
}
export const runMigrations = async (
migrations: Migration[],
options: MigrationOptions = {}
) => {
let tenantIds
if (environment.MULTI_TENANCY) {
if (options.noOp) {
tenantIds = [options.noOp.tenantId]
} else if (!options.tenantIds || !options.tenantIds.length) {
// run for all tenants
tenantIds = await getTenantIds()
} else {
tenantIds = options.tenantIds
}
} else {
// single tenancy
tenantIds = [DEFAULT_TENANT_ID]
}
if (tenantIds.length > 1) {
console.log(`Checking migrations for ${tenantIds.length} tenants`)
} else {
console.log("Checking migrations")
}
let count = 0
// for all tenants
for (const tenantId of tenantIds) {
count++
if (tenantIds.length > 1) {
console.log(`Progress [${count}/${tenantIds.length}]`)
}
// for all migrations
for (const migration of migrations) {
// run the migration
await doInTenant(tenantId, () => runMigration(migration, options))
}
}
console.log("Migrations complete")
}

View File

@ -1,4 +0,0 @@
module.exports = {
...require("../context"),
...require("./tenancy"),
}

View File

@ -0,0 +1,9 @@
import * as context from "../context"
import * as tenancy from "./tenancy"
const pkg = {
...context,
...tenancy,
}
export = pkg

View File

@ -1,18 +1,18 @@
const { doWithDB } = require("../db") import { doWithDB } from "../db"
const { StaticDatabases } = require("../db/constants") import { StaticDatabases } from "../db/constants"
const { baseGlobalDBName } = require("./utils") import { baseGlobalDBName } from "./utils"
const { import {
getTenantId, getTenantId,
DEFAULT_TENANT_ID, DEFAULT_TENANT_ID,
isMultiTenant, isMultiTenant,
getTenantIDFromAppID, getTenantIDFromAppID,
} = require("../context") } from "../context"
const env = require("../environment") import env from "../environment"
const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants
const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name
exports.addTenantToUrl = url => { export const addTenantToUrl = (url: string) => {
const tenantId = getTenantId() const tenantId = getTenantId()
if (isMultiTenant()) { if (isMultiTenant()) {
@ -23,8 +23,8 @@ exports.addTenantToUrl = url => {
return url return url
} }
exports.doesTenantExist = async tenantId => { export const doesTenantExist = async (tenantId: string) => {
return doWithDB(PLATFORM_INFO_DB, async db => { return doWithDB(PLATFORM_INFO_DB, async (db: any) => {
let tenants let tenants
try { try {
tenants = await db.get(TENANT_DOC) tenants = await db.get(TENANT_DOC)
@ -40,9 +40,14 @@ exports.doesTenantExist = async tenantId => {
}) })
} }
exports.tryAddTenant = async (tenantId, userId, email) => { export const tryAddTenant = async (
return doWithDB(PLATFORM_INFO_DB, async db => { tenantId: string,
const getDoc = async id => { userId: string,
email: string,
afterCreateTenant: () => Promise<void>
) => {
return doWithDB(PLATFORM_INFO_DB, async (db: any) => {
const getDoc = async (id: string) => {
if (!id) { if (!id) {
return null return null
} }
@ -76,12 +81,13 @@ exports.tryAddTenant = async (tenantId, userId, email) => {
if (tenants.tenantIds.indexOf(tenantId) === -1) { if (tenants.tenantIds.indexOf(tenantId) === -1) {
tenants.tenantIds.push(tenantId) tenants.tenantIds.push(tenantId)
promises.push(db.put(tenants)) promises.push(db.put(tenants))
await afterCreateTenant()
} }
await Promise.all(promises) await Promise.all(promises)
}) })
} }
exports.getGlobalDBName = (tenantId = null) => { export const getGlobalDBName = (tenantId?: string) => {
// tenant ID can be set externally, for example user API where // tenant ID can be set externally, for example user API where
// new tenants are being created, this may be the case // new tenants are being created, this may be the case
if (!tenantId) { if (!tenantId) {
@ -90,12 +96,12 @@ exports.getGlobalDBName = (tenantId = null) => {
return baseGlobalDBName(tenantId) return baseGlobalDBName(tenantId)
} }
exports.doWithGlobalDB = (tenantId, cb) => { export const doWithGlobalDB = (tenantId: string, cb: any) => {
return doWithDB(exports.getGlobalDBName(tenantId), cb) return doWithDB(getGlobalDBName(tenantId), cb)
} }
exports.lookupTenantId = async userId => { export const lookupTenantId = async (userId: string) => {
return doWithDB(StaticDatabases.PLATFORM_INFO.name, async db => { return doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: any) => {
let tenantId = env.MULTI_TENANCY ? DEFAULT_TENANT_ID : null let tenantId = env.MULTI_TENANCY ? DEFAULT_TENANT_ID : null
try { try {
const doc = await db.get(userId) const doc = await db.get(userId)
@ -110,8 +116,8 @@ exports.lookupTenantId = async userId => {
} }
// lookup, could be email or userId, either will return a doc // lookup, could be email or userId, either will return a doc
exports.getTenantUser = async identifier => { export const getTenantUser = async (identifier: string) => {
return doWithDB(PLATFORM_INFO_DB, async db => { return doWithDB(PLATFORM_INFO_DB, async (db: any) => {
try { try {
return await db.get(identifier) return await db.get(identifier)
} catch (err) { } catch (err) {
@ -120,7 +126,7 @@ exports.getTenantUser = async identifier => {
}) })
} }
exports.isUserInAppTenant = (appId, user = null) => { export const isUserInAppTenant = (appId: string, user: any) => {
let userTenantId let userTenantId
if (user) { if (user) {
userTenantId = user.tenantId || DEFAULT_TENANT_ID userTenantId = user.tenantId || DEFAULT_TENANT_ID
@ -131,8 +137,8 @@ exports.isUserInAppTenant = (appId, user = null) => {
return tenantId === userTenantId return tenantId === userTenantId
} }
exports.getTenantIds = async () => { export const getTenantIds = async () => {
return doWithDB(PLATFORM_INFO_DB, async db => { return doWithDB(PLATFORM_INFO_DB, async (db: any) => {
let tenants let tenants
try { try {
tenants = await db.get(TENANT_DOC) tenants = await db.get(TENANT_DOC)

View File

@ -52,8 +52,8 @@ const {
} = require("@budibase/backend-core/context") } = require("@budibase/backend-core/context")
import { getUniqueRows } from "../../utilities/usageQuota/rows" import { getUniqueRows } from "../../utilities/usageQuota/rows"
import { quotas } from "@budibase/pro" import { quotas } from "@budibase/pro"
import { errors, events } from "@budibase/backend-core" import { errors, events, migrations } from "@budibase/backend-core"
import { App } from "@budibase/types" import { App, MigrationType } from "@budibase/types"
const URL_REGEX_SLASH = /\/|\\/g const URL_REGEX_SLASH = /\/|\\/g
@ -319,6 +319,12 @@ const creationEvents = async (request: any, app: App) => {
} }
const appPostCreate = async (ctx: any, app: App) => { const appPostCreate = async (ctx: any, app: App) => {
const tenantId = getTenantId()
await migrations.backPopulateMigrations({
type: MigrationType.APP,
tenantId,
appId: app.appId,
})
await creationEvents(ctx.request, app) await creationEvents(ctx.request, app)
// app import & template creation // app import & template creation
if (ctx.request.body.useTemplate === "true") { if (ctx.request.body.useTemplate === "true") {

View File

@ -1,4 +1,6 @@
import { migrations, redis } from "@budibase/backend-core" import { migrations, redis } from "@budibase/backend-core"
import { Migration, MigrationOptions, MigrationName } from "@budibase/types"
import env from "../environment"
// migration functions // migration functions
import * as userEmailViewCasing from "./functions/userEmailViewCasing" import * as userEmailViewCasing from "./functions/userEmailViewCasing"
@ -7,83 +9,88 @@ import * as appUrls from "./functions/appUrls"
import * as developerQuota from "./functions/developerQuota" import * as developerQuota from "./functions/developerQuota"
import * as publishedAppsQuota from "./functions/publishedAppsQuota" import * as publishedAppsQuota from "./functions/publishedAppsQuota"
import * as backfill from "./functions/backfill" import * as backfill from "./functions/backfill"
import env from "../environment"
export interface Migration {
type: string
name: string
opts?: object
fn: Function
silent?: boolean
preventRetry?: boolean
}
/** /**
* e.g. * Populate the migration function and additional configuration from
* { * the static migration definitions.
* tenantIds: ['bb'],
* force: {
* global: ['quota_1']
* }
* }
*/ */
export interface MigrationOptions { export const buildMigrations = () => {
tenantIds?: string[] const definitions = migrations.DEFINITIONS
force?: { const serverMigrations: Migration[] = []
[type: string]: string[]
for (const definition of definitions) {
switch (definition.name) {
case MigrationName.USER_EMAIL_VIEW_CASING: {
serverMigrations.push({
...definition,
fn: userEmailViewCasing.run,
})
break
}
case MigrationName.QUOTAS_1: {
serverMigrations.push({
...definition,
fn: quota1.run,
})
break
}
case MigrationName.APP_URLS: {
serverMigrations.push({
...definition,
appOpts: { all: true },
fn: appUrls.run,
})
break
}
case MigrationName.DEVELOPER_QUOTA: {
serverMigrations.push({
...definition,
fn: developerQuota.run,
})
break
}
case MigrationName.PUBLISHED_APP_QUOTA: {
serverMigrations.push({
...definition,
fn: publishedAppsQuota.run,
})
break
}
case MigrationName.EVENT_APP_BACKFILL: {
serverMigrations.push({
...definition,
appOpts: { all: true },
fn: backfill.app.run,
silent: !!env.SELF_HOSTED, // reduce noisy logging
preventRetry: !!env.SELF_HOSTED, // only ever run once
})
break
}
case MigrationName.EVENT_GLOBAL_BACKFILL: {
serverMigrations.push({
...definition,
fn: backfill.global.run,
silent: !!env.SELF_HOSTED, // reduce noisy logging
preventRetry: !!env.SELF_HOSTED, // only ever run once
})
break
}
case MigrationName.EVENT_INSTALLATION_BACKFILL: {
serverMigrations.push({
...definition,
fn: backfill.installation.run,
silent: !!env.SELF_HOSTED, // reduce noisy logging
preventRetry: !!env.SELF_HOSTED, // only ever run once
})
break
}
}
} }
return serverMigrations
} }
export const MIGRATIONS: Migration[] = [ export const MIGRATIONS = buildMigrations()
{
type: migrations.MIGRATION_TYPES.GLOBAL,
name: "user_email_view_casing",
fn: userEmailViewCasing.run,
},
{
type: migrations.MIGRATION_TYPES.GLOBAL,
name: "quotas_1",
fn: quota1.run,
},
{
type: migrations.MIGRATION_TYPES.APP,
name: "app_urls",
opts: { all: true },
fn: appUrls.run,
},
{
type: migrations.MIGRATION_TYPES.GLOBAL,
name: "developer_quota",
fn: developerQuota.run,
},
{
type: migrations.MIGRATION_TYPES.GLOBAL,
name: "published_apps_quota",
fn: publishedAppsQuota.run,
},
{
type: migrations.MIGRATION_TYPES.APP,
name: "event_app_backfill",
opts: { all: true },
fn: backfill.app.run,
silent: !!env.SELF_HOSTED, // reduce noisy logging
preventRetry: !!env.SELF_HOSTED, // only ever run once
},
{
type: migrations.MIGRATION_TYPES.GLOBAL,
name: "event_global_backfill",
fn: backfill.global.run,
silent: !!env.SELF_HOSTED, // reduce noisy logging
preventRetry: !!env.SELF_HOSTED, // only ever run once
},
{
type: migrations.MIGRATION_TYPES.INSTALLATION,
name: "event_installation_backfill",
fn: backfill.installation.run,
silent: !!env.SELF_HOSTED, // reduce noisy logging
preventRetry: !!env.SELF_HOSTED, // only ever run once
},
]
export const migrate = async (options?: MigrationOptions) => { export const migrate = async (options?: MigrationOptions) => {
if (env.SELF_HOSTED) { if (env.SELF_HOSTED) {

View File

@ -1,4 +1,11 @@
import { events, migrations, tenancy } from "@budibase/backend-core" import {
events,
migrations,
tenancy,
DocumentTypes,
context,
db,
} from "@budibase/backend-core"
import TestConfig from "../../tests/utilities/TestConfiguration" import TestConfig from "../../tests/utilities/TestConfiguration"
import structures from "../../tests/utilities/structures" import structures from "../../tests/utilities/structures"
import { MIGRATIONS } from "../" import { MIGRATIONS } from "../"
@ -7,6 +14,15 @@ import * as helpers from "./helpers"
const { mocks } = require("@budibase/backend-core/tests") const { mocks } = require("@budibase/backend-core/tests")
const timestamp = mocks.date.MOCK_DATE.toISOString() const timestamp = mocks.date.MOCK_DATE.toISOString()
const clearMigrations = async () => {
const dbs = [context.getDevAppDB(), context.getProdAppDB()]
for (const db of dbs) {
const doc = await db.get(DocumentTypes.MIGRATIONS)
const newDoc = { _id: doc._id, _rev: doc._rev }
await db.put(newDoc)
}
}
describe("migrations", () => { describe("migrations", () => {
const config = new TestConfig() const config = new TestConfig()
@ -21,6 +37,7 @@ describe("migrations", () => {
describe("backfill", () => { describe("backfill", () => {
it("runs app db migration", async () => { it("runs app db migration", async () => {
await config.doInContext(null, async () => { await config.doInContext(null, async () => {
await clearMigrations()
await config.createAutomation() await config.createAutomation()
await config.createAutomation(structures.newAutomation()) await config.createAutomation(structures.newAutomation())
await config.createDatasource() await config.createDatasource()
@ -71,6 +88,7 @@ describe("migrations", () => {
it("runs global db migration", async () => { it("runs global db migration", async () => {
await config.doInContext(null, async () => { await config.doInContext(null, async () => {
await clearMigrations()
const appId = config.prodAppId const appId = config.prodAppId
const roles = { [appId]: "role_12345" } const roles = { [appId]: "role_12345" }
await config.createUser(undefined, undefined, false, true, roles) // admin only await config.createUser(undefined, undefined, false, true, roles) // admin only

View File

@ -1,3 +0,0 @@
export * from "./hosting"
export * from "./context"
export * from "./identification"

View File

@ -1,4 +1,4 @@
import { Hosting } from "../../core" import { Hosting } from "../../sdk"
export interface CreateAccount { export interface CreateAccount {
email: string email: string

View File

@ -1,4 +1,4 @@
export * from "./documents" export * from "./documents"
export * from "./events" export * from "./sdk/events"
export * from "./licensing" export * from "./sdk/licensing"
export * from "./core" export * from "./sdk"

View File

@ -1,5 +1,5 @@
import { User, Account } from "../documents" import { User, Account } from "../documents"
import { IdentityType } from "./identification" import { IdentityType } from "./events/identification"
export interface BaseContext { export interface BaseContext {
_id: string _id: string

View File

@ -1,4 +1,4 @@
import { Hosting } from "." import { Hosting } from ".."
// GROUPS // GROUPS

View File

@ -17,3 +17,4 @@ export * from "./user"
export * from "./view" export * from "./view"
export * from "./account" export * from "./account"
export * from "./backfill" export * from "./backfill"
export * from "./identification"

View File

@ -1,4 +1,4 @@
import { ViewCalculation } from "../documents" import { ViewCalculation } from "../../documents"
import { BaseEvent, TableExportFormat } from "./event" import { BaseEvent, TableExportFormat } from "./event"
export interface ViewCreatedEvent extends BaseEvent { export interface ViewCreatedEvent extends BaseEvent {

View File

@ -0,0 +1,5 @@
export * from "./hosting"
export * from "./context"
export * from "./events"
export * from "./licensing"
export * from "./migrations"

View File

@ -0,0 +1,54 @@
export interface Migration extends MigrationDefinition {
appOpts?: object
fn: Function
silent?: boolean
preventRetry?: boolean
}
export enum MigrationType {
// run once per tenant, recorded in global db, global db is provided as an argument
GLOBAL = "global",
// run per app, recorded in each app db, app db is provided as an argument
APP = "app",
// run once, recorded in global info db, global info db is provided as an argument
INSTALLATION = "installation",
}
export interface MigrationNoOpOptions {
type: MigrationType
tenantId: string
appId?: string
}
/**
* e.g.
* {
* tenantIds: ['bb'],
* force: {
* global: ['quota_1']
* }
* }
*/
export interface MigrationOptions {
tenantIds?: string[]
force?: {
[type: string]: string[]
}
noOp?: MigrationNoOpOptions
}
export enum MigrationName {
USER_EMAIL_VIEW_CASING = "user_email_view_casing",
QUOTAS_1 = "quotas_1",
APP_URLS = "app_urls",
DEVELOPER_QUOTA = "developer_quota",
PUBLISHED_APP_QUOTA = "published_apps_quota",
EVENT_APP_BACKFILL = "event_app_backfill",
EVENT_GLOBAL_BACKFILL = "event_global_backfill",
EVENT_INSTALLATION_BACKFILL = "event_installation_backfill",
}
export interface MigrationDefinition {
type: MigrationType
name: MigrationName
}

View File

@ -13,7 +13,9 @@ import {
sessions, sessions,
HTTPError, HTTPError,
accounts, accounts,
migrations,
} from "@budibase/backend-core" } from "@budibase/backend-core"
import { MigrationType } from "@budibase/types"
/** /**
* Retrieves all users from the current tenancy. * Retrieves all users from the current tenancy.
@ -146,7 +148,12 @@ export const save = async (
await eventHelpers.handleSaveEvents(user, dbUser) await eventHelpers.handleSaveEvents(user, dbUser)
if (env.MULTI_TENANCY) { if (env.MULTI_TENANCY) {
await tenancy.tryAddTenant(tenantId, _id, email) const afterCreateTenant = () =>
migrations.backPopulateMigrations({
type: MigrationType.GLOBAL,
tenantId,
})
await tenancy.tryAddTenant(tenantId, _id, email, afterCreateTenant)
} }
await cache.user.invalidateUser(response.id) await cache.user.invalidateUser(response.id)
// let server know to sync user // let server know to sync user

View File

@ -13,6 +13,10 @@ cd packages/types
yarn link yarn link
cd - cd -
echo "Linking bbui"
cd packages/bbui
yarn link
cd -
if [ -d "../budibase-pro" ]; then if [ -d "../budibase-pro" ]; then
cd ../budibase-pro cd ../budibase-pro
@ -52,5 +56,7 @@ if [ -d "../account-portal" ]; then
yarn link "@budibase/pro" yarn link "@budibase/pro"
fi fi
cd - cd ../ui
echo "Linking bbui to account-portal"
yarn link "@budibase/bbui"
fi fi