diff --git a/packages/builder/src/components/start/CreateAppModal.svelte b/packages/builder/src/components/start/CreateAppModal.svelte index b5fed9cdc1..83ed089006 100644 --- a/packages/builder/src/components/start/CreateAppModal.svelte +++ b/packages/builder/src/components/start/CreateAppModal.svelte @@ -118,14 +118,17 @@ if ($values.url) { data.append("url", $values.url.trim()) } - data.append("useTemplate", template != null) - if (template) { - data.append("templateName", template.name) - data.append("templateKey", template.key) - data.append("templateFile", $values.file) + + if (template?.fromFile) { + data.append("useTemplate", true) + data.append("fileToImport", $values.file) if ($values.encryptionPassword?.trim()) { data.append("encryptionPassword", $values.encryptionPassword.trim()) } + } else if (template) { + data.append("useTemplate", true) + data.append("templateName", template.name) + data.append("templateKey", template.key) } // Create App diff --git a/packages/server/scripts/load/utils.js b/packages/server/scripts/load/utils.js index 1dabdcec9a..57e03c471c 100644 --- a/packages/server/scripts/load/utils.js +++ b/packages/server/scripts/load/utils.js @@ -29,7 +29,7 @@ exports.createApp = async apiKey => { const body = { name, url: `/${name}`, - useTemplate: "true", + useTemplate: true, templateKey: "app/school-admin-panel", templateName: "School Admin Panel", } diff --git a/packages/server/src/api/controllers/application.ts b/packages/server/src/api/controllers/application.ts index 59f67540fe..d8d9d75336 100644 --- a/packages/server/src/api/controllers/application.ts +++ b/packages/server/src/api/controllers/application.ts @@ -23,6 +23,7 @@ import { cache, context, db as dbCore, + docIds, env as envCore, ErrorCode, events, @@ -35,7 +36,6 @@ import { import { USERS_TABLE_SCHEMA, DEFAULT_BB_DATASOURCE_ID } from "../../constants" import { buildDefaultDocs } from "../../db/defaultData/datasource_bb_default" import { removeAppFromUserRoles } from "../../utilities/workerRequests" -import { stringToReadStream } from "../../utilities" import { doesUserHaveLock } from "../../utilities/redis" import { cleanupAutomations } from "../../automations/utils" import { getUniqueRows } from "../../utilities/usageQuota/rows" @@ -54,6 +54,11 @@ import { DuplicateAppResponse, UpdateAppRequest, UpdateAppResponse, + Database, + FieldType, + BBReferenceFieldSubType, + Row, + BBRequest, } from "@budibase/types" import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts" import sdk from "../../sdk" @@ -123,8 +128,7 @@ function checkAppName( } interface AppTemplate { - templateString?: string - useTemplate?: string + useTemplate?: boolean file?: { type?: string path: string @@ -148,14 +152,7 @@ async function createInstance(appId: string, template: AppTemplate) { await createRoutingView() await createAllSearchIndex() - // replicate the template data to the instance DB - // this is currently very hard to test, downloading and importing template files - if (template && template.templateString) { - const { ok } = await db.load(stringToReadStream(template.templateString)) - if (!ok) { - throw "Error loading database dump from memory." - } - } else if (template && template.useTemplate === "true") { + if (template && template.useTemplate) { await sdk.backups.importApp(appId, db, template) } else { // create the users table @@ -243,14 +240,15 @@ export async function fetchAppPackage( async function performAppCreate(ctx: UserCtx) { const apps = (await dbCore.getAllApps({ dev: true })) as App[] - const { - name, - url, - encryptionPassword, - useTemplate, - templateKey, - templateString, - } = ctx.request.body + const { body } = ctx.request + const { name, url, encryptionPassword, templateKey } = body + + let useTemplate + if (typeof body.useTemplate === "string") { + useTemplate = body.useTemplate === "true" + } else if (typeof body.useTemplate === "boolean") { + useTemplate = body.useTemplate + } checkAppName(ctx, apps, name) const appUrl = sdk.applications.getAppUrl({ name, url }) @@ -259,16 +257,15 @@ async function performAppCreate(ctx: UserCtx) { const instanceConfig: AppTemplate = { useTemplate, key: templateKey, - templateString, } - if (ctx.request.files && ctx.request.files.templateFile) { + if (ctx.request.files && ctx.request.files.fileToImport) { instanceConfig.file = { - ...(ctx.request.files.templateFile as any), + ...(ctx.request.files.fileToImport as any), password: encryptionPassword, } - } else if (typeof ctx.request.body.file?.path === "string") { + } else if (typeof body.file?.path === "string") { instanceConfig.file = { - path: ctx.request.body.file?.path, + path: body.file?.path, } } @@ -279,6 +276,10 @@ async function performAppCreate(ctx: UserCtx) { const instance = await createInstance(appId, instanceConfig) const db = context.getAppDB() + if (instanceConfig.useTemplate && !instanceConfig.file) { + await updateUserColumns(appId, db, ctx.user._id!) + } + const newApplication: App = { _id: DocumentType.APP_METADATA, _rev: undefined, @@ -375,21 +376,81 @@ async function performAppCreate(ctx: UserCtx) { }) } -async function creationEvents(request: any, app: App) { +async function updateUserColumns( + appId: string, + db: Database, + toUserId: string +) { + await context.doInAppContext(appId, async () => { + const allTables = await sdk.tables.getAllTables() + const tablesWithUserColumns = [] + for (const table of allTables) { + const userColumns = Object.values(table.schema).filter( + f => + (f.type === FieldType.BB_REFERENCE || + f.type === FieldType.BB_REFERENCE_SINGLE) && + f.subtype === BBReferenceFieldSubType.USER + ) + if (!userColumns.length) { + continue + } + + tablesWithUserColumns.push({ + tableId: table._id!, + columns: userColumns.map(c => c.name), + }) + } + + const docsToUpdate = [] + + for (const { tableId, columns } of tablesWithUserColumns) { + const docs = await db.allDocs( + docIds.getRowParams(tableId, null, { include_docs: true }) + ) + const rows = docs.rows.map(d => d.doc!) + + for (const row of rows) { + let shouldUpdate = false + const updatedColumns = columns.reduce((newColumns, column) => { + if (row[column]) { + shouldUpdate = true + if (Array.isArray(row[column])) { + newColumns[column] = row[column]?.map(() => toUserId) + } else if (row[column]) { + newColumns[column] = toUserId + } + } + return newColumns + }, {}) + + if (shouldUpdate) { + docsToUpdate.push({ + ...row, + ...updatedColumns, + }) + } + } + } + + await db.bulkDocs(docsToUpdate) + }) +} + +async function creationEvents(request: BBRequest, app: App) { let creationFns: ((app: App) => Promise)[] = [] - const body = request.body - if (body.useTemplate === "true") { + const { useTemplate, templateKey, file } = request.body + if (useTemplate === "true") { // from template - if (body.templateKey && body.templateKey !== "undefined") { - creationFns.push(a => events.app.templateImported(a, body.templateKey)) + if (templateKey && templateKey !== "undefined") { + creationFns.push(a => events.app.templateImported(a, templateKey)) } // from file - else if (request.files?.templateFile) { + else if (request.files?.fileToImport) { creationFns.push(a => events.app.fileImported(a)) } // from server file path - else if (request.body.file) { + else if (file) { // explicitly pass in the newly created app id creationFns.push(a => events.app.duplicated(a, app.appId)) } @@ -399,16 +460,14 @@ async function creationEvents(request: any, app: App) { } } - if (!request.duplicate) { - creationFns.push(a => events.app.created(a)) - } + creationFns.push(a => events.app.created(a)) for (let fn of creationFns) { await fn(app) } } -async function appPostCreate(ctx: UserCtx, app: App) { +async function appPostCreate(ctx: UserCtx, app: App) { const tenantId = tenancy.getTenantId() await migrations.backPopulateMigrations({ type: MigrationType.APP, @@ -419,7 +478,7 @@ async function appPostCreate(ctx: UserCtx, app: App) { await creationEvents(ctx.request, app) // app import, template creation and duplication - if (ctx.request.body.useTemplate === "true") { + if (ctx.request.body.useTemplate) { const { rows } = await getUniqueRows([app.appId]) const rowCount = rows ? rows.length : 0 if (rowCount) { diff --git a/packages/server/src/api/routes/tests/application.spec.ts b/packages/server/src/api/routes/tests/application.spec.ts index fe8250bde5..47b6776610 100644 --- a/packages/server/src/api/routes/tests/application.spec.ts +++ b/packages/server/src/api/routes/tests/application.spec.ts @@ -21,6 +21,7 @@ import tk from "timekeeper" import * as uuid from "uuid" import { structures } from "@budibase/backend-core/tests" import nock from "nock" +import path from "path" describe("/applications", () => { let config = setup.getConfig() @@ -137,11 +138,17 @@ describe("/applications", () => { }) it("creates app from template", async () => { + nock("https://prod-budi-templates.s3-eu-west-1.amazonaws.com") + .get(`/templates/app/agency-client-portal.tar.gz`) + .replyWithFile( + 200, + path.resolve(__dirname, "data", "agency-client-portal.tar.gz") + ) + const app = await config.api.application.create({ name: utils.newid(), useTemplate: "true", - templateKey: "test", - templateString: "{}", + templateKey: "app/agency-client-portal", }) expect(app._id).toBeDefined() expect(events.app.created).toHaveBeenCalledTimes(1) @@ -152,7 +159,7 @@ describe("/applications", () => { const app = await config.api.application.create({ name: utils.newid(), useTemplate: "true", - templateFile: "src/api/routes/tests/data/export.txt", + fileToImport: "src/api/routes/tests/data/export.txt", }) expect(app._id).toBeDefined() expect(events.app.created).toHaveBeenCalledTimes(1) @@ -172,7 +179,7 @@ describe("/applications", () => { const app = await config.api.application.create({ name: utils.newid(), useTemplate: "true", - templateFile: "src/api/routes/tests/data/old-app.txt", + fileToImport: "src/api/routes/tests/data/old-app.txt", }) expect(app._id).toBeDefined() expect(app.navigation).toBeDefined() diff --git a/packages/server/src/api/routes/utils/validators.ts b/packages/server/src/api/routes/utils/validators.ts index d9dbd96b16..02a1a2d060 100644 --- a/packages/server/src/api/routes/utils/validators.ts +++ b/packages/server/src/api/routes/utils/validators.ts @@ -355,9 +355,7 @@ export function applicationValidator(opts = { isCreate: true }) { _id: OPTIONAL_STRING, _rev: OPTIONAL_STRING, url: OPTIONAL_STRING, - template: Joi.object({ - templateString: OPTIONAL_STRING, - }), + template: Joi.object({}), } const appNameValidator = Joi.string() @@ -390,9 +388,7 @@ export function applicationValidator(opts = { isCreate: true }) { _rev: OPTIONAL_STRING, name: appNameValidator, url: OPTIONAL_STRING, - template: Joi.object({ - templateString: OPTIONAL_STRING, - }).unknown(true), + template: Joi.object({}).unknown(true), snippets: snippetValidator, }).unknown(true) ) diff --git a/packages/server/src/tests/utilities/api/application.ts b/packages/server/src/tests/utilities/api/application.ts index 516af5c973..9dabc8cfe8 100644 --- a/packages/server/src/tests/utilities/api/application.ts +++ b/packages/server/src/tests/utilities/api/application.ts @@ -17,8 +17,8 @@ export class ApplicationAPI extends TestAPI { app: CreateAppRequest, expectations?: Expectations ): Promise => { - const files = app.templateFile ? { templateFile: app.templateFile } : {} - delete app.templateFile + const files = app.fileToImport ? { fileToImport: app.fileToImport } : {} + delete app.fileToImport return await this._post("/api/applications", { fields: app, files, diff --git a/packages/server/src/utilities/index.ts b/packages/server/src/utilities/index.ts index ce6f2345ca..129137a72e 100644 --- a/packages/server/src/utilities/index.ts +++ b/packages/server/src/utilities/index.ts @@ -2,9 +2,6 @@ import env from "../environment" import { context } from "@budibase/backend-core" import { generateMetadataID } from "../db/utils" import { Document } from "@budibase/types" -import stream from "stream" - -const Readable = stream.Readable export function wait(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)) @@ -98,15 +95,6 @@ export function escapeDangerousCharacters(string: string) { .replace(/[\t]/g, "\\t") } -export function stringToReadStream(string: string) { - return new Readable({ - read() { - this.push(string) - this.push(null) - }, - }) -} - export function formatBytes(bytes: string) { const units = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] const byteIncrements = 1024 diff --git a/packages/types/src/api/web/application.ts b/packages/types/src/api/web/application.ts index bb4d8c7f72..57422ceabc 100644 --- a/packages/types/src/api/web/application.ts +++ b/packages/types/src/api/web/application.ts @@ -7,10 +7,8 @@ export interface CreateAppRequest { useTemplate?: string templateName?: string templateKey?: string - templateFile?: string - includeSampleData?: boolean + fileToImport?: string encryptionPassword?: string - templateString?: string file?: { path: string } }