Migrate ApplicationAPI

This commit is contained in:
Sam Rose 2024-02-28 17:27:15 +00:00
parent 3e76511ffd
commit 7a48fd85ac
No known key found for this signature in database
6 changed files with 149 additions and 159 deletions

View File

@ -184,7 +184,7 @@ describe("/applications", () => {
it("app should not sync if production", async () => { it("app should not sync if production", async () => {
const { message } = await config.api.application.sync( const { message } = await config.api.application.sync(
app.appId.replace("_dev", ""), app.appId.replace("_dev", ""),
{ statusCode: 400 } { status: 400 }
) )
expect(message).toEqual( expect(message).toEqual(

View File

@ -30,9 +30,9 @@ describe("migrations", () => {
const appId = config.getAppId() const appId = config.getAppId()
const response = await config.api.application.getRaw(appId) await config.api.application.get(appId, {
headersNotPresent: [Header.MIGRATING_APP],
expect(response.headers[Header.MIGRATING_APP]).toBeUndefined() })
}) })
it("accessing an app that has pending migrations will attach the migrating header", async () => { it("accessing an app that has pending migrations will attach the migrating header", async () => {
@ -46,8 +46,10 @@ describe("migrations", () => {
func: async () => {}, func: async () => {},
}) })
const response = await config.api.application.getRaw(appId) await config.api.application.get(appId, {
headers: {
expect(response.headers[Header.MIGRATING_APP]).toEqual(appId) [Header.MIGRATING_APP]: appId,
},
})
}) })
}) })

View File

@ -1,12 +1,12 @@
import { Response } from "supertest"
import { import {
App, App,
PublishResponse,
type CreateAppRequest, type CreateAppRequest,
type FetchAppDefinitionResponse, type FetchAppDefinitionResponse,
type FetchAppPackageResponse, type FetchAppPackageResponse,
} from "@budibase/types" } from "@budibase/types"
import TestConfiguration from "../TestConfiguration" import TestConfiguration from "../TestConfiguration"
import { TestAPI } from "./base" import { Expectations, TestAPI } from "./base"
import { AppStatus } from "../../../db/utils" import { AppStatus } from "../../../db/utils"
import { constants } from "@budibase/backend-core" import { constants } from "@budibase/backend-core"
@ -15,179 +15,124 @@ export class ApplicationAPI extends TestAPI {
super(config) super(config)
} }
create = async (app: CreateAppRequest): Promise<App> => { create = async (
const request = this.request app: CreateAppRequest,
.post("/api/applications") expectations?: Expectations
.set(this.config.defaultHeaders()) ): Promise<App> => {
.expect("Content-Type", /json/) const files = app.templateFile ? { templateFile: app.templateFile } : {}
delete app.templateFile
for (const key of Object.keys(app)) { return await this._post<App>("/api/applications", {
request.field(key, (app as any)[key]) fields: app,
files,
expectations,
})
} }
if (app.templateFile) { delete = async (
request.attach("templateFile", app.templateFile) appId: string,
expectations?: Expectations
): Promise<void> => {
await this._delete(`/api/applications/${appId}`, { expectations })
} }
const result = await request publish = async (appId: string): Promise<PublishResponse> => {
return await this._post<PublishResponse>(
if (result.statusCode !== 200) { `/api/applications/${appId}/publish`,
throw new Error(JSON.stringify(result.body)) {
}
return result.body as App
}
delete = async (appId: string): Promise<void> => {
await this.request
.delete(`/api/applications/${appId}`)
.set(this.config.defaultHeaders())
.expect(200)
}
publish = async (
appId: string
): Promise<{ _id: string; status: string; appUrl: string }> => {
// While the publish endpoint does take an :appId parameter, it doesn't // While the publish endpoint does take an :appId parameter, it doesn't
// use it. It uses the appId from the context. // use it. It uses the appId from the context.
let headers = { headers: {
...this.config.defaultHeaders(),
[constants.Header.APP_ID]: appId, [constants.Header.APP_ID]: appId,
},
} }
const result = await this.request )
.post(`/api/applications/${appId}/publish`)
.set(headers)
.expect("Content-Type", /json/)
.expect(200)
return result.body as { _id: string; status: string; appUrl: string }
} }
unpublish = async (appId: string): Promise<void> => { unpublish = async (appId: string): Promise<void> => {
await this.request await this._post(`/api/applications/${appId}/unpublish`, {
.post(`/api/applications/${appId}/unpublish`) expectations: { status: 204 },
.set(this.config.defaultHeaders()) })
.expect(204)
} }
sync = async ( sync = async (
appId: string, appId: string,
{ statusCode }: { statusCode: number } = { statusCode: 200 } expectations?: Expectations
): Promise<{ message: string }> => { ): Promise<{ message: string }> => {
const result = await this.request return await this._post<{ message: string }>(
.post(`/api/applications/${appId}/sync`) `/api/applications/${appId}/sync`,
.set(this.config.defaultHeaders()) { expectations }
.expect("Content-Type", /json/) )
.expect(statusCode)
return result.body
} }
getRaw = async (appId: string): Promise<Response> => { get = async (appId: string, expectations?: Expectations): Promise<App> => {
// While the appPackage endpoint does take an :appId parameter, it doesn't return await this._get<App>(`/api/applications/${appId}`, {
// use it. It uses the appId from the context. // While the get endpoint does take an :appId parameter, it doesn't use
let headers = { // it. It uses the appId from the context.
...this.config.defaultHeaders(), headers: {
[constants.Header.APP_ID]: appId, [constants.Header.APP_ID]: appId,
} },
const result = await this.request expectations,
.get(`/api/applications/${appId}/appPackage`) })
.set(headers)
.expect("Content-Type", /json/)
.expect(200)
return result
}
get = async (appId: string): Promise<App> => {
const result = await this.getRaw(appId)
return result.body.application as App
} }
getDefinition = async ( getDefinition = async (
appId: string appId: string,
expectations?: Expectations
): Promise<FetchAppDefinitionResponse> => { ): Promise<FetchAppDefinitionResponse> => {
const result = await this.request return await this._get<FetchAppDefinitionResponse>(
.get(`/api/applications/${appId}/definition`) `/api/applications/${appId}/definition`,
.set(this.config.defaultHeaders()) { expectations }
.expect("Content-Type", /json/) )
.expect(200)
return result.body as FetchAppDefinitionResponse
} }
getAppPackage = async (appId: string): Promise<FetchAppPackageResponse> => { getAppPackage = async (
const result = await this.request appId: string,
.get(`/api/applications/${appId}/appPackage`) expectations?: Expectations
.set(this.config.defaultHeaders()) ): Promise<FetchAppPackageResponse> => {
.expect("Content-Type", /json/) return await this._get<FetchAppPackageResponse>(
.expect(200) `/api/applications/${appId}/appPackage`,
return result.body { expectations }
)
} }
update = async ( update = async (
appId: string, appId: string,
app: { name?: string; url?: string } app: { name?: string; url?: string },
expectations?: Expectations
): Promise<App> => { ): Promise<App> => {
const request = this.request return await this._put<App>(`/api/applications/${appId}`, {
.put(`/api/applications/${appId}`) fields: app,
.set(this.config.defaultHeaders()) expectations,
.expect("Content-Type", /json/) })
for (const key of Object.keys(app)) {
request.field(key, (app as any)[key])
} }
const result = await request updateClient = async (
appId: string,
if (result.statusCode !== 200) { expectations?: Expectations
throw new Error(JSON.stringify(result.body)) ): Promise<void> => {
} await this._post(`/api/applications/${appId}/client/update`, {
return result.body as App
}
updateClient = async (appId: string): Promise<void> => {
// While the updateClient endpoint does take an :appId parameter, it doesn't // While the updateClient endpoint does take an :appId parameter, it doesn't
// use it. It uses the appId from the context. // use it. It uses the appId from the context.
let headers = { headers: {
...this.config.defaultHeaders(),
[constants.Header.APP_ID]: appId, [constants.Header.APP_ID]: appId,
} },
const response = await this.request expectations,
.post(`/api/applications/${appId}/client/update`) })
.set(headers)
.expect("Content-Type", /json/)
if (response.statusCode !== 200) {
throw new Error(JSON.stringify(response.body))
}
} }
revertClient = async (appId: string): Promise<void> => { revertClient = async (appId: string): Promise<void> => {
await this._post(`/api/applications/${appId}/client/revert`, {
// While the revertClient endpoint does take an :appId parameter, it doesn't // While the revertClient endpoint does take an :appId parameter, it doesn't
// use it. It uses the appId from the context. // use it. It uses the appId from the context.
let headers = { headers: {
...this.config.defaultHeaders(),
[constants.Header.APP_ID]: appId, [constants.Header.APP_ID]: appId,
} },
const response = await this.request })
.post(`/api/applications/${appId}/client/revert`)
.set(headers)
.expect("Content-Type", /json/)
if (response.statusCode !== 200) {
throw new Error(JSON.stringify(response.body))
}
} }
fetch = async ({ status }: { status?: AppStatus } = {}): Promise<App[]> => { fetch = async ({ status }: { status?: AppStatus } = {}): Promise<App[]> => {
let query = [] return await this._get<App[]>("/api/applications", {
if (status) { query: { status },
query.push(`status=${status}`) })
}
const result = await this.request
.get(`/api/applications${query.length ? `?${query.join("&")}` : ""}`)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
return result.body as App[]
} }
} }

View File

@ -1,3 +1,4 @@
import exp from "constants"
import TestConfiguration from "../TestConfiguration" import TestConfiguration from "../TestConfiguration"
import { SuperTest, Test } from "supertest" import { SuperTest, Test } from "supertest"
@ -11,10 +12,13 @@ export interface TestAPIOpts {
export interface Expectations { export interface Expectations {
status?: number status?: number
contentType?: string | RegExp contentType?: string | RegExp
headers?: Record<string, string | RegExp>
headersNotPresent?: string[]
} }
export interface RequestOpts { export interface RequestOpts {
headers?: Headers headers?: Headers
query?: Record<string, string | undefined>
body?: Record<string, any> body?: Record<string, any>
fields?: Record<string, any> fields?: Record<string, any>
files?: Record<string, any> files?: Record<string, any>
@ -30,11 +34,8 @@ export abstract class TestAPI {
this.request = config.request! this.request = config.request!
} }
protected _get = async <T>( protected _get = async <T>(url: string, opts?: RequestOpts): Promise<T> => {
url: string, return await this._request<T>("get", url, opts)
expectations?: Expectations
): Promise<T> => {
return await this._request<T>("get", url, { expectations })
} }
protected _post = async <T>(url: string, opts?: RequestOpts): Promise<T> => { protected _post = async <T>(url: string, opts?: RequestOpts): Promise<T> => {
@ -61,9 +62,26 @@ export abstract class TestAPI {
url: string, url: string,
opts?: RequestOpts opts?: RequestOpts
): Promise<T> => { ): Promise<T> => {
const { headers = {}, body, fields, files, expectations } = opts || {} const {
headers = {},
query = {},
body,
fields,
files,
expectations,
} = opts || {}
const { status = 200, contentType = /json/ } = expectations || {} const { status = 200, contentType = /json/ } = expectations || {}
let queryParams = []
for (const [key, value] of Object.entries(query)) {
if (value) {
queryParams.push(`${key}=${value}`)
}
}
if (queryParams.length) {
url += `?${queryParams.join("&")}`
}
let request = this.request[method](url).set(this.config.defaultHeaders()) let request = this.request[method](url).set(this.config.defaultHeaders())
if (headers) { if (headers) {
request = request.set(headers) request = request.set(headers)
@ -81,13 +99,22 @@ export abstract class TestAPI {
request = request.attach(key, value) request = request.attach(key, value)
} }
} }
if (contentType) { if (contentType && status !== 204) {
if (contentType instanceof RegExp) { if (contentType instanceof RegExp) {
request = request.expect("Content-Type", contentType) request = request.expect("Content-Type", contentType)
} else { } else {
request = request.expect("Content-Type", contentType) request = request.expect("Content-Type", contentType)
} }
} }
if (expectations?.headers) {
for (const [key, value] of Object.entries(expectations.headers)) {
if (value instanceof RegExp) {
request = request.expect(key, value)
} else {
request = request.expect(key, value)
}
}
}
const response = await request const response = await request
@ -99,6 +126,16 @@ export abstract class TestAPI {
) )
} }
if (expectations?.headersNotPresent) {
for (const header of expectations.headersNotPresent) {
if (response.headers[header]) {
throw new Error(
`Expected header ${header} not to be present, found value "${response.headers[header]}"`
)
}
}
}
return response.body as T return response.body as T
} }
} }

View File

@ -19,11 +19,11 @@ export class TableAPI extends TestAPI {
} }
fetch = async (expectations?: Expectations): Promise<Table[]> => { fetch = async (expectations?: Expectations): Promise<Table[]> => {
return await this._get<Table[]>("/api/tables", expectations) return await this._get<Table[]>("/api/tables", { expectations })
} }
get = async (tableId: string, expectations: Expectations): Promise<Table> => { get = async (tableId: string, expectations: Expectations): Promise<Table> => {
return await this._get<Table>(`/api/tables/${tableId}`, expectations) return await this._get<Table>(`/api/tables/${tableId}`, { expectations })
} }
migrate = async ( migrate = async (

View File

@ -27,3 +27,9 @@ export interface FetchAppPackageResponse {
clientLibPath: string clientLibPath: string
hasLock: boolean hasLock: boolean
} }
export interface PublishResponse {
_id: string
status: string
appUrl: string
}