Merge pull request #15027 from Budibase/fix/use-direct-describe

Direct describe rather than datasourceDescribe
This commit is contained in:
Michael Drury 2024-11-20 15:29:08 +00:00 committed by GitHub
commit 0a3000e3ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 14372 additions and 14185 deletions

View File

@ -164,9 +164,12 @@ describe("/datasources", () => {
})
})
datasourceDescribe(
{ name: "%s", exclude: [DatabaseName.MONGODB, DatabaseName.SQS] },
({ config, dsProvider }) => {
const descriptions = datasourceDescribe({
exclude: [DatabaseName.MONGODB, DatabaseName.SQS],
})
if (descriptions.length) {
describe.each(descriptions)("$dbName", ({ config, dsProvider }) => {
let datasource: Datasource
let rawDatasource: Datasource
let client: Knex
@ -492,5 +495,5 @@ datasourceDescribe(
)
})
})
}
)
})
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -977,63 +977,69 @@ describe("/rowsActions", () => {
})
})
datasourceDescribe(
{ name: "row actions (%s)", only: [DatabaseName.SQS, DatabaseName.POSTGRES] },
({ config, dsProvider, isInternal }) => {
let datasource: Datasource | undefined
const descriptions = datasourceDescribe({
only: [DatabaseName.SQS, DatabaseName.POSTGRES],
})
beforeAll(async () => {
const ds = await dsProvider()
datasource = ds.datasource
})
if (descriptions.length) {
describe.each(descriptions)(
"row actions ($dbName)",
({ config, dsProvider, isInternal }) => {
let datasource: Datasource | undefined
async function getTable(): Promise<Table> {
if (isInternal) {
await config.api.application.addSampleData(config.getAppId())
const tables = await config.api.table.fetch()
return tables.find(t => t.sourceId === DEFAULT_BB_DATASOURCE_ID)!
} else {
const table = await config.api.table.save(
setup.structures.tableForDatasource(datasource!)
)
return table
}
}
beforeAll(async () => {
const ds = await dsProvider()
datasource = ds.datasource
})
it("should delete all the row actions (and automations) for its tables when a datasource is deleted", async () => {
async function getRowActionsFromDb(tableId: string) {
return await context.doInAppContext(config.getAppId(), async () => {
const db = context.getAppDB()
const tableDoc = await db.tryGet<TableRowActions>(
generateRowActionsID(tableId)
async function getTable(): Promise<Table> {
if (isInternal) {
await config.api.application.addSampleData(config.getAppId())
const tables = await config.api.table.fetch()
return tables.find(t => t.sourceId === DEFAULT_BB_DATASOURCE_ID)!
} else {
const table = await config.api.table.save(
setup.structures.tableForDatasource(datasource!)
)
return tableDoc
})
return table
}
}
const table = await getTable()
const tableId = table._id!
it("should delete all the row actions (and automations) for its tables when a datasource is deleted", async () => {
async function getRowActionsFromDb(tableId: string) {
return await context.doInAppContext(config.getAppId(), async () => {
const db = context.getAppDB()
const tableDoc = await db.tryGet<TableRowActions>(
generateRowActionsID(tableId)
)
return tableDoc
})
}
await config.api.rowAction.save(tableId, {
name: generator.guid(),
const table = await getTable()
const tableId = table._id!
await config.api.rowAction.save(tableId, {
name: generator.guid(),
})
await config.api.rowAction.save(tableId, {
name: generator.guid(),
})
const { actions } = (await getRowActionsFromDb(tableId))!
expect(Object.entries(actions)).toHaveLength(2)
const { automations } = await config.api.automation.fetch()
expect(automations).toHaveLength(2)
const datasource = await config.api.datasource.get(table.sourceId)
await config.api.datasource.delete(datasource)
const automationsResp = await config.api.automation.fetch()
expect(automationsResp.automations).toHaveLength(0)
expect(await getRowActionsFromDb(tableId)).toBeUndefined()
})
await config.api.rowAction.save(tableId, {
name: generator.guid(),
})
const { actions } = (await getRowActionsFromDb(tableId))!
expect(Object.entries(actions)).toHaveLength(2)
const { automations } = await config.api.automation.fetch()
expect(automations).toHaveLength(2)
const datasource = await config.api.datasource.get(table.sourceId)
await config.api.datasource.delete(datasource)
const automationsResp = await config.api.automation.fetch()
expect(automationsResp.automations).toHaveLength(0)
expect(await getRowActionsFromDb(tableId)).toBeUndefined()
})
}
)
}
)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -7,71 +7,74 @@ import {
import { Knex } from "knex"
import { generator } from "@budibase/backend-core/tests"
datasourceDescribe(
{
name: "execute query action",
exclude: [DatabaseName.MONGODB, DatabaseName.SQS],
},
({ config, dsProvider }) => {
let tableName: string
let client: Knex
let datasource: Datasource
let query: Query
const descriptions = datasourceDescribe({
exclude: [DatabaseName.MONGODB, DatabaseName.SQS],
})
beforeAll(async () => {
const ds = await dsProvider()
datasource = ds.datasource!
client = ds.client!
})
if (descriptions.length) {
describe.each(descriptions)(
"execute query action ($dbName)",
({ config, dsProvider }) => {
let tableName: string
let client: Knex
let datasource: Datasource
let query: Query
beforeEach(async () => {
tableName = generator.guid()
await client.schema.createTable(tableName, table => {
table.string("a")
table.integer("b")
beforeAll(async () => {
const ds = await dsProvider()
datasource = ds.datasource!
client = ds.client!
})
await client(tableName).insert({ a: "string", b: 1 })
query = await setup.saveTestQuery(config, client, tableName, datasource)
})
afterEach(async () => {
await client.schema.dropTable(tableName)
})
beforeEach(async () => {
tableName = generator.guid()
await client.schema.createTable(tableName, table => {
table.string("a")
table.integer("b")
})
await client(tableName).insert({ a: "string", b: 1 })
query = await setup.saveTestQuery(config, client, tableName, datasource)
})
it("should be able to execute a query", async () => {
let res = await setup.runStep(
config,
setup.actions.EXECUTE_QUERY.stepId,
{
query: { queryId: query._id },
}
)
expect(res.response).toEqual([{ a: "string", b: 1 }])
expect(res.success).toEqual(true)
})
afterEach(async () => {
await client.schema.dropTable(tableName)
})
it("should handle a null query value", async () => {
let res = await setup.runStep(
config,
setup.actions.EXECUTE_QUERY.stepId,
{
query: null,
}
)
expect(res.response.message).toEqual("Invalid inputs")
expect(res.success).toEqual(false)
})
it("should be able to execute a query", async () => {
let res = await setup.runStep(
config,
setup.actions.EXECUTE_QUERY.stepId,
{
query: { queryId: query._id },
}
)
expect(res.response).toEqual([{ a: "string", b: 1 }])
expect(res.success).toEqual(true)
})
it("should handle an error executing a query", async () => {
let res = await setup.runStep(
config,
setup.actions.EXECUTE_QUERY.stepId,
{
query: { queryId: "wrong_id" },
}
)
expect(res.response).toBeDefined()
expect(res.success).toEqual(false)
})
}
)
it("should handle a null query value", async () => {
let res = await setup.runStep(
config,
setup.actions.EXECUTE_QUERY.stepId,
{
query: null,
}
)
expect(res.response.message).toEqual("Invalid inputs")
expect(res.success).toEqual(false)
})
it("should handle an error executing a query", async () => {
let res = await setup.runStep(
config,
setup.actions.EXECUTE_QUERY.stepId,
{
query: { queryId: "wrong_id" },
}
)
expect(res.response).toBeDefined()
expect(res.success).toEqual(false)
})
}
)
}

View File

@ -433,9 +433,10 @@ describe("Automation Scenarios", () => {
})
})
datasourceDescribe(
{ name: "", only: [DatabaseName.MYSQL] },
({ config, dsProvider }) => {
const descriptions = datasourceDescribe({ only: [DatabaseName.MYSQL] })
if (descriptions.length) {
describe.each(descriptions)("/rows ($dbName)", ({ config, dsProvider }) => {
let datasource: Datasource
let client: Knex
@ -531,5 +532,5 @@ datasourceDescribe(
)
})
})
}
)
})
}

View File

@ -10,119 +10,123 @@ function uniqueTableName(length?: number): string {
.substring(0, length || 10)
}
datasourceDescribe(
{
name: "Integration compatibility with mysql search_path",
only: [DatabaseName.MYSQL],
},
({ config, dsProvider }) => {
let rawDatasource: Datasource
let datasource: Datasource
let client: Knex
const mainDescriptions = datasourceDescribe({ only: [DatabaseName.MYSQL] })
const database = generator.guid()
const database2 = generator.guid()
if (mainDescriptions.length) {
describe.each(mainDescriptions)(
"/Integration compatibility with mysql search_path ($dbName)",
({ config, dsProvider }) => {
let rawDatasource: Datasource
let datasource: Datasource
let client: Knex
beforeAll(async () => {
const ds = await dsProvider()
rawDatasource = ds.rawDatasource!
datasource = ds.datasource!
client = ds.client!
const database = generator.guid()
const database2 = generator.guid()
await client.raw(`CREATE DATABASE \`${database}\`;`)
await client.raw(`CREATE DATABASE \`${database2}\`;`)
beforeAll(async () => {
const ds = await dsProvider()
rawDatasource = ds.rawDatasource!
datasource = ds.datasource!
client = ds.client!
rawDatasource.config!.database = database
datasource = await config.api.datasource.create(rawDatasource)
})
await client.raw(`CREATE DATABASE \`${database}\`;`)
await client.raw(`CREATE DATABASE \`${database2}\`;`)
afterAll(async () => {
await client.raw(`DROP DATABASE \`${database}\`;`)
await client.raw(`DROP DATABASE \`${database2}\`;`)
})
it("discovers tables from any schema in search path", async () => {
await client.schema.createTable(`${database}.table1`, table => {
table.increments("id1").primary()
rawDatasource.config!.database = database
datasource = await config.api.datasource.create(rawDatasource)
})
const res = await config.api.datasource.info(datasource)
expect(res.tableNames).toBeDefined()
expect(res.tableNames).toEqual(expect.arrayContaining(["table1"]))
})
it("does not mix columns from different tables", async () => {
const repeated_table_name = "table_same_name"
await client.schema.createTable(
`${database}.${repeated_table_name}`,
table => {
table.increments("id").primary()
table.string("val1")
}
)
await client.schema.createTable(
`${database2}.${repeated_table_name}`,
table => {
table.increments("id2").primary()
table.string("val2")
}
)
const res = await config.api.datasource.fetchSchema({
datasourceId: datasource._id!,
tablesFilter: [repeated_table_name],
afterAll(async () => {
await client.raw(`DROP DATABASE \`${database}\`;`)
await client.raw(`DROP DATABASE \`${database2}\`;`)
})
expect(res.datasource.entities![repeated_table_name].schema).toBeDefined()
const schema = res.datasource.entities![repeated_table_name].schema
expect(Object.keys(schema).sort()).toEqual(["id", "val1"])
})
}
)
datasourceDescribe(
{
name: "POST /api/datasources/:datasourceId/schema",
only: [DatabaseName.MYSQL],
},
({ config, dsProvider }) => {
let datasource: Datasource
let client: Knex
it("discovers tables from any schema in search path", async () => {
await client.schema.createTable(`${database}.table1`, table => {
table.increments("id1").primary()
})
const res = await config.api.datasource.info(datasource)
expect(res.tableNames).toBeDefined()
expect(res.tableNames).toEqual(expect.arrayContaining(["table1"]))
})
beforeAll(async () => {
const ds = await dsProvider()
datasource = ds.datasource!
client = ds.client!
})
let tableName: string
beforeEach(async () => {
tableName = uniqueTableName()
})
afterEach(async () => {
await client.schema.dropTableIfExists(tableName)
})
it("recognises enum columns as options", async () => {
const enumColumnName = "status"
await client.schema.createTable(tableName, table => {
table.increments("order_id").primary()
table.string("customer_name", 100).notNullable()
table.enum(
enumColumnName,
["pending", "processing", "shipped", "delivered", "cancelled"],
{ useNative: true, enumName: `${tableName}_${enumColumnName}` }
it("does not mix columns from different tables", async () => {
const repeated_table_name = "table_same_name"
await client.schema.createTable(
`${database}.${repeated_table_name}`,
table => {
table.increments("id").primary()
table.string("val1")
}
)
await client.schema.createTable(
`${database2}.${repeated_table_name}`,
table => {
table.increments("id2").primary()
table.string("val2")
}
)
const res = await config.api.datasource.fetchSchema({
datasourceId: datasource._id!,
tablesFilter: [repeated_table_name],
})
expect(
res.datasource.entities![repeated_table_name].schema
).toBeDefined()
const schema = res.datasource.entities![repeated_table_name].schema
expect(Object.keys(schema).sort()).toEqual(["id", "val1"])
})
}
)
const res = await config.api.datasource.fetchSchema({
datasourceId: datasource._id!,
})
const descriptions = datasourceDescribe({ only: [DatabaseName.MYSQL] })
const table = res.datasource.entities![tableName]
if (descriptions.length) {
describe.each(descriptions)(
"POST /api/datasources/:datasourceId/schema ($dbName)",
({ config, dsProvider }) => {
let datasource: Datasource
let client: Knex
expect(table).toBeDefined()
expect(table.schema[enumColumnName].type).toEqual(FieldType.OPTIONS)
})
beforeAll(async () => {
const ds = await dsProvider()
datasource = ds.datasource!
client = ds.client!
})
let tableName: string
beforeEach(async () => {
tableName = uniqueTableName()
})
afterEach(async () => {
await client.schema.dropTableIfExists(tableName)
})
it("recognises enum columns as options", async () => {
const enumColumnName = "status"
await client.schema.createTable(tableName, table => {
table.increments("order_id").primary()
table.string("customer_name", 100).notNullable()
table.enum(
enumColumnName,
["pending", "processing", "shipped", "delivered", "cancelled"],
{ useNative: true, enumName: `${tableName}_${enumColumnName}` }
)
})
const res = await config.api.datasource.fetchSchema({
datasourceId: datasource._id!,
})
const table = res.datasource.entities![tableName]
expect(table).toBeDefined()
expect(table.schema[enumColumnName].type).toEqual(FieldType.OPTIONS)
})
}
)
}
)
}

View File

@ -8,283 +8,292 @@ import {
} from "../integrations/tests/utils"
import { Knex } from "knex"
datasourceDescribe(
{ name: "postgres integrations", only: [DatabaseName.POSTGRES] },
({ config, dsProvider }) => {
let datasource: Datasource
let client: Knex
const mainDescriptions = datasourceDescribe({ only: [DatabaseName.POSTGRES] })
beforeAll(async () => {
const ds = await dsProvider()
datasource = ds.datasource!
client = ds.client!
})
if (mainDescriptions.length) {
describe.each(mainDescriptions)(
"/postgres integrations",
({ config, dsProvider }) => {
let datasource: Datasource
let client: Knex
afterAll(config.end)
describe("POST /api/datasources/:datasourceId/schema", () => {
let tableName: string
beforeEach(async () => {
tableName = generator.guid().replaceAll("-", "").substring(0, 10)
beforeAll(async () => {
const ds = await dsProvider()
datasource = ds.datasource!
client = ds.client!
})
afterEach(async () => {
await client.schema.dropTableIfExists(tableName)
})
afterAll(config.end)
it("recognises when a table has no primary key", async () => {
await client.schema.createTable(tableName, table => {
table.increments("id", { primaryKey: false })
describe("POST /api/datasources/:datasourceId/schema", () => {
let tableName: string
beforeEach(async () => {
tableName = generator.guid().replaceAll("-", "").substring(0, 10)
})
const response = await config.api.datasource.fetchSchema({
datasourceId: datasource._id!,
afterEach(async () => {
await client.schema.dropTableIfExists(tableName)
})
expect(response.errors).toEqual({
[tableName]: "Table must have a primary key.",
})
})
it("recognises when a table has no primary key", async () => {
await client.schema.createTable(tableName, table => {
table.increments("id", { primaryKey: false })
})
it("recognises when a table is using a reserved column name", async () => {
await client.schema.createTable(tableName, table => {
table.increments("_id").primary()
})
const response = await config.api.datasource.fetchSchema({
datasourceId: datasource._id!,
})
const response = await config.api.datasource.fetchSchema({
datasourceId: datasource._id!,
})
expect(response.errors).toEqual({
[tableName]: "Table contains invalid columns.",
})
})
it("recognises enum columns as options", async () => {
const tableName = `orders_${generator
.guid()
.replaceAll("-", "")
.substring(0, 6)}`
await client.schema.createTable(tableName, table => {
table.increments("order_id").primary()
table.string("customer_name").notNullable()
table.enum("status", ["pending", "processing", "shipped"], {
useNative: true,
enumName: `${tableName}_status`,
expect(response.errors).toEqual({
[tableName]: "Table must have a primary key.",
})
})
const response = await config.api.datasource.fetchSchema({
datasourceId: datasource._id!,
it("recognises when a table is using a reserved column name", async () => {
await client.schema.createTable(tableName, table => {
table.increments("_id").primary()
})
const response = await config.api.datasource.fetchSchema({
datasourceId: datasource._id!,
})
expect(response.errors).toEqual({
[tableName]: "Table contains invalid columns.",
})
})
const table = response.datasource.entities?.[tableName]
it("recognises enum columns as options", async () => {
const tableName = `orders_${generator
.guid()
.replaceAll("-", "")
.substring(0, 6)}`
expect(table).toBeDefined()
expect(table?.schema["status"].type).toEqual(FieldType.OPTIONS)
})
})
await client.schema.createTable(tableName, table => {
table.increments("order_id").primary()
table.string("customer_name").notNullable()
table.enum("status", ["pending", "processing", "shipped"], {
useNative: true,
enumName: `${tableName}_status`,
})
})
describe("check custom column types", () => {
beforeAll(async () => {
await client.schema.createTable("binaryTable", table => {
table.binary("id").primary()
table.string("column1")
table.integer("column2")
const response = await config.api.datasource.fetchSchema({
datasourceId: datasource._id!,
})
const table = response.datasource.entities?.[tableName]
expect(table).toBeDefined()
expect(table?.schema["status"].type).toEqual(FieldType.OPTIONS)
})
})
it("should handle binary columns", async () => {
const response = await config.api.datasource.fetchSchema({
datasourceId: datasource._id!,
describe("check custom column types", () => {
beforeAll(async () => {
await client.schema.createTable("binaryTable", table => {
table.binary("id").primary()
table.string("column1")
table.integer("column2")
})
})
expect(response.datasource.entities).toBeDefined()
const table = response.datasource.entities?.["binaryTable"]
expect(table).toBeDefined()
expect(table?.schema.id.externalType).toBe("bytea")
const row = await config.api.row.save(table?._id!, {
id: "1111",
column1: "hello",
column2: 222,
})
expect(row._id).toBeDefined()
const decoded = decodeURIComponent(row._id!).replace(/'/g, '"')
expect(JSON.parse(decoded)[0]).toBe("1111")
})
})
describe("check fetching null/not null table", () => {
beforeAll(async () => {
await client.schema.createTable("nullableTable", table => {
table.increments("order_id").primary()
table.integer("order_number").notNullable()
it("should handle binary columns", async () => {
const response = await config.api.datasource.fetchSchema({
datasourceId: datasource._id!,
})
expect(response.datasource.entities).toBeDefined()
const table = response.datasource.entities?.["binaryTable"]
expect(table).toBeDefined()
expect(table?.schema.id.externalType).toBe("bytea")
const row = await config.api.row.save(table?._id!, {
id: "1111",
column1: "hello",
column2: 222,
})
expect(row._id).toBeDefined()
const decoded = decodeURIComponent(row._id!).replace(/'/g, '"')
expect(JSON.parse(decoded)[0]).toBe("1111")
})
})
it("should be able to change the table to allow nullable and refetch this", async () => {
const response = await config.api.datasource.fetchSchema({
datasourceId: datasource._id!,
})
const entities = response.datasource.entities
expect(entities).toBeDefined()
const nullableTable = entities?.["nullableTable"]
expect(nullableTable).toBeDefined()
expect(
nullableTable?.schema["order_number"].constraints?.presence
).toEqual(true)
// need to perform these calls raw to the DB so that the external state of the DB differs to what Budibase
// is aware of - therefore we can try to fetch and make sure BB updates correctly
await client.schema.alterTable("nullableTable", table => {
table.setNullable("order_number")
describe("check fetching null/not null table", () => {
beforeAll(async () => {
await client.schema.createTable("nullableTable", table => {
table.increments("order_id").primary()
table.integer("order_number").notNullable()
})
})
const responseAfter = await config.api.datasource.fetchSchema({
datasourceId: datasource._id!,
it("should be able to change the table to allow nullable and refetch this", async () => {
const response = await config.api.datasource.fetchSchema({
datasourceId: datasource._id!,
})
const entities = response.datasource.entities
expect(entities).toBeDefined()
const nullableTable = entities?.["nullableTable"]
expect(nullableTable).toBeDefined()
expect(
nullableTable?.schema["order_number"].constraints?.presence
).toEqual(true)
// need to perform these calls raw to the DB so that the external state of the DB differs to what Budibase
// is aware of - therefore we can try to fetch and make sure BB updates correctly
await client.schema.alterTable("nullableTable", table => {
table.setNullable("order_number")
})
const responseAfter = await config.api.datasource.fetchSchema({
datasourceId: datasource._id!,
})
const entitiesAfter = responseAfter.datasource.entities
expect(entitiesAfter).toBeDefined()
const nullableTableAfter = entitiesAfter?.["nullableTable"]
expect(nullableTableAfter).toBeDefined()
expect(
nullableTableAfter?.schema["order_number"].constraints?.presence
).toBeUndefined()
})
const entitiesAfter = responseAfter.datasource.entities
expect(entitiesAfter).toBeDefined()
const nullableTableAfter = entitiesAfter?.["nullableTable"]
expect(nullableTableAfter).toBeDefined()
expect(
nullableTableAfter?.schema["order_number"].constraints?.presence
).toBeUndefined()
})
})
describe("money field 💰", () => {
const tableName = "moneytable"
let table: Table
describe("money field 💰", () => {
const tableName = "moneytable"
let table: Table
beforeAll(async () => {
await client.raw(`
beforeAll(async () => {
await client.raw(`
CREATE TABLE ${tableName} (
id serial PRIMARY KEY,
price money
)
`)
const response = await config.api.datasource.fetchSchema({
datasourceId: datasource._id!,
})
table = response.datasource.entities![tableName]
})
it("should be able to import a money field", async () => {
expect(table).toBeDefined()
expect(table?.schema.price.type).toBe(FieldType.NUMBER)
})
it("should be able to search a money field", async () => {
await config.api.row.bulkImport(table._id!, {
rows: [{ price: 200 }, { price: 300 }],
const response = await config.api.datasource.fetchSchema({
datasourceId: datasource._id!,
})
table = response.datasource.entities![tableName]
})
const { rows } = await config.api.row.search(table._id!, {
query: {
equal: {
price: 200,
it("should be able to import a money field", async () => {
expect(table).toBeDefined()
expect(table?.schema.price.type).toBe(FieldType.NUMBER)
})
it("should be able to search a money field", async () => {
await config.api.row.bulkImport(table._id!, {
rows: [{ price: 200 }, { price: 300 }],
})
const { rows } = await config.api.row.search(table._id!, {
query: {
equal: {
price: 200,
},
},
},
})
expect(rows).toHaveLength(1)
expect(rows[0].price).toBe("200.00")
})
it("should be able to update a money field", async () => {
let row = await config.api.row.save(table._id!, { price: 200 })
expect(row.price).toBe("200.00")
row = await config.api.row.save(table._id!, { ...row, price: 300 })
expect(row.price).toBe("300.00")
row = await config.api.row.save(table._id!, {
...row,
price: "400.00",
})
expect(row.price).toBe("400.00")
})
expect(rows).toHaveLength(1)
expect(rows[0].price).toBe("200.00")
})
}
)
it("should be able to update a money field", async () => {
let row = await config.api.row.save(table._id!, { price: 200 })
expect(row.price).toBe("200.00")
const descriptions = datasourceDescribe({ only: [DatabaseName.POSTGRES] })
row = await config.api.row.save(table._id!, { ...row, price: 300 })
expect(row.price).toBe("300.00")
if (descriptions.length) {
describe.each(descriptions)(
"Integration compatibility with postgres search_path",
({ config, dsProvider }) => {
let datasource: Datasource
let client: Knex
let schema1: string
let schema2: string
row = await config.api.row.save(table._id!, { ...row, price: "400.00" })
expect(row.price).toBe("400.00")
})
})
beforeEach(async () => {
const ds = await dsProvider()
datasource = ds.datasource!
const rawDatasource = ds.rawDatasource!
schema1 = generator.guid().replaceAll("-", "")
schema2 = generator.guid().replaceAll("-", "")
client = await knexClient(rawDatasource)
await client.schema.createSchema(schema1)
await client.schema.createSchema(schema2)
rawDatasource.config!.schema = `${schema1}, ${schema2}`
client = await knexClient(rawDatasource)
datasource = await config.api.datasource.create(rawDatasource)
})
afterEach(async () => {
await client.schema.dropSchema(schema1, true)
await client.schema.dropSchema(schema2, true)
})
it("discovers tables from any schema in search path", async () => {
await client.schema.createTable(`${schema1}.table1`, table => {
table.increments("id1").primary()
})
await client.schema.createTable(`${schema2}.table2`, table => {
table.increments("id2").primary()
})
const response = await config.api.datasource.info(datasource)
expect(response.tableNames).toBeDefined()
expect(response.tableNames).toEqual(
expect.arrayContaining(["table1", "table2"])
)
})
it("does not mix columns from different tables", async () => {
const repeated_table_name = "table_same_name"
await client.schema.createTable(
`${schema1}.${repeated_table_name}`,
table => {
table.increments("id").primary()
table.string("val1")
}
)
await client.schema.createTable(
`${schema2}.${repeated_table_name}`,
table => {
table.increments("id2").primary()
table.string("val2")
}
)
const response = await config.api.datasource.fetchSchema({
datasourceId: datasource._id!,
tablesFilter: [repeated_table_name],
})
expect(
response.datasource.entities?.[repeated_table_name].schema
).toBeDefined()
const schema =
response.datasource.entities?.[repeated_table_name].schema
expect(Object.keys(schema || {}).sort()).toEqual(["id", "val1"])
})
}
)
}
)
datasourceDescribe(
{
name: "Integration compatibility with postgres search_path",
only: [DatabaseName.POSTGRES],
},
({ config, dsProvider }) => {
let datasource: Datasource
let client: Knex
let schema1: string
let schema2: string
beforeEach(async () => {
const ds = await dsProvider()
datasource = ds.datasource!
const rawDatasource = ds.rawDatasource!
schema1 = generator.guid().replaceAll("-", "")
schema2 = generator.guid().replaceAll("-", "")
client = await knexClient(rawDatasource)
await client.schema.createSchema(schema1)
await client.schema.createSchema(schema2)
rawDatasource.config!.schema = `${schema1}, ${schema2}`
client = await knexClient(rawDatasource)
datasource = await config.api.datasource.create(rawDatasource)
})
afterEach(async () => {
await client.schema.dropSchema(schema1, true)
await client.schema.dropSchema(schema2, true)
})
it("discovers tables from any schema in search path", async () => {
await client.schema.createTable(`${schema1}.table1`, table => {
table.increments("id1").primary()
})
await client.schema.createTable(`${schema2}.table2`, table => {
table.increments("id2").primary()
})
const response = await config.api.datasource.info(datasource)
expect(response.tableNames).toBeDefined()
expect(response.tableNames).toEqual(
expect.arrayContaining(["table1", "table2"])
)
})
it("does not mix columns from different tables", async () => {
const repeated_table_name = "table_same_name"
await client.schema.createTable(
`${schema1}.${repeated_table_name}`,
table => {
table.increments("id").primary()
table.string("val1")
}
)
await client.schema.createTable(
`${schema2}.${repeated_table_name}`,
table => {
table.increments("id2").primary()
table.string("val2")
}
)
const response = await config.api.datasource.fetchSchema({
datasourceId: datasource._id!,
tablesFilter: [repeated_table_name],
})
expect(
response.datasource.entities?.[repeated_table_name].schema
).toBeDefined()
const schema = response.datasource.entities?.[repeated_table_name].schema
expect(Object.keys(schema || {}).sort()).toEqual(["id", "val1"])
})
}
)
}

View File

@ -35,7 +35,6 @@ const providers: Record<DatabaseName, DatasourceProvider> = {
}
export interface DatasourceDescribeOpts {
name: string
only?: DatabaseName[]
exclude?: DatabaseName[]
}
@ -102,16 +101,12 @@ function createDummyTest() {
})
}
export function datasourceDescribe(
opts: DatasourceDescribeOpts,
cb: (args: DatasourceDescribeReturn) => void
) {
export function datasourceDescribe(opts: DatasourceDescribeOpts) {
if (process.env.DATASOURCE === "none") {
createDummyTest()
return
}
const { name, only, exclude } = opts
const { only, exclude } = opts
if (only && exclude) {
throw new Error("you can only supply one of 'only' or 'exclude'")
@ -130,36 +125,28 @@ export function datasourceDescribe(
if (databases.length === 0) {
createDummyTest()
return
}
describe.each(databases)(name, name => {
const config = new TestConfiguration()
afterAll(() => {
config.end()
})
cb({
name,
config,
dsProvider: () => createDatasources(config, name),
isInternal: name === DatabaseName.SQS,
isExternal: name !== DatabaseName.SQS,
isSql: [
DatabaseName.MARIADB,
DatabaseName.MYSQL,
DatabaseName.POSTGRES,
DatabaseName.SQL_SERVER,
DatabaseName.ORACLE,
].includes(name),
isMySQL: name === DatabaseName.MYSQL,
isPostgres: name === DatabaseName.POSTGRES,
isMongodb: name === DatabaseName.MONGODB,
isMSSQL: name === DatabaseName.SQL_SERVER,
isOracle: name === DatabaseName.ORACLE,
})
})
const config = new TestConfiguration()
return databases.map(dbName => ({
dbName,
config,
dsProvider: () => createDatasources(config, dbName),
isInternal: dbName === DatabaseName.SQS,
isExternal: dbName !== DatabaseName.SQS,
isSql: [
DatabaseName.MARIADB,
DatabaseName.MYSQL,
DatabaseName.POSTGRES,
DatabaseName.SQL_SERVER,
DatabaseName.ORACLE,
].includes(dbName),
isMySQL: dbName === DatabaseName.MYSQL,
isPostgres: dbName === DatabaseName.POSTGRES,
isMongodb: dbName === DatabaseName.MONGODB,
isMSSQL: dbName === DatabaseName.SQL_SERVER,
isOracle: dbName === DatabaseName.ORACLE,
}))
}
function getDatasource(

View File

@ -19,202 +19,206 @@ import { tableForDatasource } from "../../../../../tests/utilities/structures"
// These test cases are only for things that cannot be tested through the API
// (e.g. limiting searches to returning specific fields). If it's possible to
// test through the API, it should be done there instead.
datasourceDescribe(
{ name: "search sdk (%s)", exclude: [DatabaseName.MONGODB] },
({ config, dsProvider, isInternal }) => {
let datasource: Datasource | undefined
let table: Table
const descriptions = datasourceDescribe({ exclude: [DatabaseName.MONGODB] })
beforeAll(async () => {
const ds = await dsProvider()
datasource = ds.datasource
})
if (descriptions.length) {
describe.each(descriptions)(
"search sdk ($dbName)",
({ config, dsProvider, isInternal }) => {
let datasource: Datasource | undefined
let table: Table
beforeEach(async () => {
const idFieldSchema: NumberFieldMetadata | AutoColumnFieldMetadata =
isInternal
? {
name: "id",
type: FieldType.AUTO,
subtype: AutoFieldSubType.AUTO_ID,
autocolumn: true,
}
: {
name: "id",
type: FieldType.NUMBER,
autocolumn: true,
}
table = await config.api.table.save(
tableForDatasource(datasource, {
primary: ["id"],
schema: {
id: idFieldSchema,
name: {
name: "name",
type: FieldType.STRING,
},
surname: {
name: "surname",
type: FieldType.STRING,
},
age: {
name: "age",
type: FieldType.NUMBER,
},
address: {
name: "address",
type: FieldType.STRING,
},
},
})
)
for (let i = 0; i < 10; i++) {
await config.api.row.save(table._id!, {
name: generator.first(),
surname: generator.last(),
age: generator.age(),
address: generator.address(),
})
}
})
afterAll(async () => {
config.end()
})
it("querying by fields will always return data attribute columns", async () => {
await config.doInContext(config.appId, async () => {
const { rows } = await search({
tableId: table._id!,
query: {},
fields: ["name", "age"],
})
expect(rows).toHaveLength(10)
for (const row of rows) {
const keys = Object.keys(row)
expect(keys).toContain("name")
expect(keys).toContain("age")
expect(keys).not.toContain("surname")
expect(keys).not.toContain("address")
}
beforeAll(async () => {
const ds = await dsProvider()
datasource = ds.datasource
})
})
!isInternal &&
it("will decode _id in oneOf query", async () => {
await config.doInContext(config.appId, async () => {
const result = await search({
tableId: table._id!,
query: {
oneOf: {
_id: ["%5B1%5D", "%5B4%5D", "%5B8%5D"],
beforeEach(async () => {
const idFieldSchema: NumberFieldMetadata | AutoColumnFieldMetadata =
isInternal
? {
name: "id",
type: FieldType.AUTO,
subtype: AutoFieldSubType.AUTO_ID,
autocolumn: true,
}
: {
name: "id",
type: FieldType.NUMBER,
autocolumn: true,
}
table = await config.api.table.save(
tableForDatasource(datasource, {
primary: ["id"],
schema: {
id: idFieldSchema,
name: {
name: "name",
type: FieldType.STRING,
},
surname: {
name: "surname",
type: FieldType.STRING,
},
age: {
name: "age",
type: FieldType.NUMBER,
},
address: {
name: "address",
type: FieldType.STRING,
},
},
})
)
expect(result.rows).toHaveLength(3)
expect(result.rows.map(row => row.id)).toEqual(
expect.arrayContaining([1, 4, 8])
)
})
})
it("does not allow accessing hidden fields", async () => {
await config.doInContext(config.appId, async () => {
await config.api.table.save({
...table,
schema: {
...table.schema,
name: {
...table.schema.name,
visible: true,
},
age: {
...table.schema.age,
visible: false,
},
},
})
const result = await search({
tableId: table._id!,
query: {},
})
expect(result.rows).toHaveLength(10)
for (const row of result.rows) {
const keys = Object.keys(row)
expect(keys).toContain("name")
expect(keys).toContain("surname")
expect(keys).toContain("address")
expect(keys).not.toContain("age")
for (let i = 0; i < 10; i++) {
await config.api.row.save(table._id!, {
name: generator.first(),
surname: generator.last(),
age: generator.age(),
address: generator.address(),
})
}
})
})
it("does not allow accessing hidden fields even if requested", async () => {
await config.doInContext(config.appId, async () => {
await config.api.table.save({
...table,
schema: {
...table.schema,
name: {
...table.schema.name,
visible: true,
},
age: {
...table.schema.age,
visible: false,
},
},
})
const result = await search({
tableId: table._id!,
query: {},
fields: ["name", "age"],
})
expect(result.rows).toHaveLength(10)
for (const row of result.rows) {
const keys = Object.keys(row)
expect(keys).toContain("name")
expect(keys).not.toContain("age")
expect(keys).not.toContain("surname")
expect(keys).not.toContain("address")
}
afterAll(async () => {
config.end()
})
})
it.each([
[["id", "name", "age"], 3],
[["name", "age"], 10],
])(
"cannot query by non search fields (fields: %s)",
async (queryFields, expectedRows) => {
it("querying by fields will always return data attribute columns", async () => {
await config.doInContext(config.appId, async () => {
const { rows } = await search({
tableId: table._id!,
query: {
$or: {
conditions: [
{
$and: {
conditions: [
{ range: { id: { low: 2, high: 4 } } },
{ range: { id: { low: 3, high: 5 } } },
],
},
},
{ equal: { id: 7 } },
],
},
},
fields: queryFields,
query: {},
fields: ["name", "age"],
})
expect(rows).toHaveLength(expectedRows)
expect(rows).toHaveLength(10)
for (const row of rows) {
const keys = Object.keys(row)
expect(keys).toContain("name")
expect(keys).toContain("age")
expect(keys).not.toContain("surname")
expect(keys).not.toContain("address")
}
})
}
)
}
)
})
!isInternal &&
it("will decode _id in oneOf query", async () => {
await config.doInContext(config.appId, async () => {
const result = await search({
tableId: table._id!,
query: {
oneOf: {
_id: ["%5B1%5D", "%5B4%5D", "%5B8%5D"],
},
},
})
expect(result.rows).toHaveLength(3)
expect(result.rows.map(row => row.id)).toEqual(
expect.arrayContaining([1, 4, 8])
)
})
})
it("does not allow accessing hidden fields", async () => {
await config.doInContext(config.appId, async () => {
await config.api.table.save({
...table,
schema: {
...table.schema,
name: {
...table.schema.name,
visible: true,
},
age: {
...table.schema.age,
visible: false,
},
},
})
const result = await search({
tableId: table._id!,
query: {},
})
expect(result.rows).toHaveLength(10)
for (const row of result.rows) {
const keys = Object.keys(row)
expect(keys).toContain("name")
expect(keys).toContain("surname")
expect(keys).toContain("address")
expect(keys).not.toContain("age")
}
})
})
it("does not allow accessing hidden fields even if requested", async () => {
await config.doInContext(config.appId, async () => {
await config.api.table.save({
...table,
schema: {
...table.schema,
name: {
...table.schema.name,
visible: true,
},
age: {
...table.schema.age,
visible: false,
},
},
})
const result = await search({
tableId: table._id!,
query: {},
fields: ["name", "age"],
})
expect(result.rows).toHaveLength(10)
for (const row of result.rows) {
const keys = Object.keys(row)
expect(keys).toContain("name")
expect(keys).not.toContain("age")
expect(keys).not.toContain("surname")
expect(keys).not.toContain("address")
}
})
})
it.each([
[["id", "name", "age"], 3],
[["name", "age"], 10],
])(
"cannot query by non search fields (fields: %s)",
async (queryFields, expectedRows) => {
await config.doInContext(config.appId, async () => {
const { rows } = await search({
tableId: table._id!,
query: {
$or: {
conditions: [
{
$and: {
conditions: [
{ range: { id: { low: 2, high: 4 } } },
{ range: { id: { low: 3, high: 5 } } },
],
},
},
{ equal: { id: 7 } },
],
},
},
fields: queryFields,
})
expect(rows).toHaveLength(expectedRows)
})
}
)
}
)
}