Merge branch 'master' into BUDI-8084/single-attachment-column-setting
This commit is contained in:
commit
7933e7f6d6
|
@ -107,9 +107,9 @@ jobs:
|
|||
- name: Test
|
||||
run: |
|
||||
if ${{ env.USE_NX_AFFECTED }}; then
|
||||
yarn test --ignore=@budibase/worker --ignore=@budibase/server --ignore=@budibase/pro --since=${{ env.NX_BASE_BRANCH }}
|
||||
yarn test --ignore=@budibase/worker --ignore=@budibase/server --since=${{ env.NX_BASE_BRANCH }}
|
||||
else
|
||||
yarn test --ignore=@budibase/worker --ignore=@budibase/server --ignore=@budibase/pro
|
||||
yarn test --ignore=@budibase/worker --ignore=@budibase/server
|
||||
fi
|
||||
|
||||
test-worker:
|
||||
|
@ -160,31 +160,6 @@ jobs:
|
|||
yarn test --scope=@budibase/server
|
||||
fi
|
||||
|
||||
test-pro:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
|
||||
steps:
|
||||
- name: Checkout repo and submodules
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: yarn
|
||||
- run: yarn --frozen-lockfile
|
||||
- name: Test
|
||||
run: |
|
||||
if ${{ env.USE_NX_AFFECTED }}; then
|
||||
yarn test --scope=@budibase/pro --since=${{ env.NX_BASE_BRANCH }}
|
||||
else
|
||||
yarn test --scope=@budibase/pro
|
||||
fi
|
||||
|
||||
integration-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
|
|
@ -7,11 +7,12 @@ module.exports = {
|
|||
|
||||
if (
|
||||
/^@budibase\/[^/]+\/.*$/.test(importPath) &&
|
||||
importPath !== "@budibase/backend-core/tests"
|
||||
importPath !== "@budibase/backend-core/tests" &&
|
||||
importPath !== "@budibase/string-templates/test/utils"
|
||||
) {
|
||||
context.report({
|
||||
node,
|
||||
message: `Importing from @budibase is not allowed, except for @budibase/backend-core/tests.`,
|
||||
message: `Importing from @budibase is not allowed, except for @budibase/backend-core/tests and @budibase/string-templates/test/utils.`,
|
||||
})
|
||||
}
|
||||
},
|
||||
|
|
|
@ -12,8 +12,6 @@ COPY .yarnrc .
|
|||
|
||||
COPY packages/server/package.json packages/server/package.json
|
||||
COPY packages/worker/package.json packages/worker/package.json
|
||||
# string-templates does not get bundled during the esbuild process, so we want to use the local version
|
||||
COPY packages/string-templates/package.json packages/string-templates/package.json
|
||||
|
||||
|
||||
COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.sh
|
||||
|
@ -26,7 +24,7 @@ RUN ./scripts/removeWorkspaceDependencies.sh packages/worker/package.json
|
|||
RUN echo '' > scripts/syncProPackage.js
|
||||
RUN jq 'del(.scripts.postinstall)' package.json > temp.json && mv temp.json package.json
|
||||
RUN ./scripts/removeWorkspaceDependencies.sh package.json
|
||||
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production
|
||||
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production --frozen-lockfile
|
||||
|
||||
# copy the actual code
|
||||
COPY packages/server/dist packages/server/dist
|
||||
|
@ -35,7 +33,6 @@ COPY packages/server/client packages/server/client
|
|||
COPY packages/server/builder packages/server/builder
|
||||
COPY packages/worker/dist packages/worker/dist
|
||||
COPY packages/worker/pm2.config.js packages/worker/pm2.config.js
|
||||
COPY packages/string-templates packages/string-templates
|
||||
|
||||
|
||||
FROM budibase/couchdb:v3.3.3 as runner
|
||||
|
@ -52,11 +49,11 @@ RUN apt-get update && \
|
|||
|
||||
# Install postgres client for pg_dump utils
|
||||
RUN apt install -y software-properties-common apt-transport-https ca-certificates gnupg \
|
||||
&& curl -fsSl https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/postgresql.gpg > /dev/null \
|
||||
&& echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main | tee /etc/apt/sources.list.d/postgresql.list \
|
||||
&& apt update -y \
|
||||
&& apt install postgresql-client-15 -y \
|
||||
&& apt remove software-properties-common apt-transport-https gpg -y
|
||||
&& curl -fsSl https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/postgresql.gpg > /dev/null \
|
||||
&& echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main | tee /etc/apt/sources.list.d/postgresql.list \
|
||||
&& apt update -y \
|
||||
&& apt install postgresql-client-15 -y \
|
||||
&& apt remove software-properties-common apt-transport-https gpg -y
|
||||
|
||||
# We use pm2 in order to run multiple node processes in a single container
|
||||
RUN npm install --global pm2
|
||||
|
@ -100,9 +97,6 @@ COPY --from=build /app/node_modules /node_modules
|
|||
COPY --from=build /app/package.json /package.json
|
||||
COPY --from=build /app/packages/server /app
|
||||
COPY --from=build /app/packages/worker /worker
|
||||
COPY --from=build /app/packages/string-templates /string-templates
|
||||
|
||||
RUN cd /string-templates && yarn link && cd ../app && yarn link @budibase/string-templates && cd ../worker && yarn link @budibase/string-templates
|
||||
|
||||
|
||||
EXPOSE 80
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "2.21.9",
|
||||
"version": "2.22.1",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*",
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 0c050591c21d3b67dc0c9225d60cc9e2324c8dac
|
||||
Subproject commit 23a1219732bd778654c0bcc4f49910c511e2d51f
|
|
@ -15,7 +15,8 @@
|
|||
"@budibase/types": ["../types/src"],
|
||||
"@budibase/backend-core": ["../backend-core/src"],
|
||||
"@budibase/backend-core/*": ["../backend-core/*"],
|
||||
"@budibase/shared-core": ["../shared-core/src"]
|
||||
"@budibase/shared-core": ["../shared-core/src"],
|
||||
"@budibase/string-templates": ["../string-templates/src"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
|
|
|
@ -1,16 +1,8 @@
|
|||
{
|
||||
"extends": "./tsconfig.build.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"ts-node": {
|
||||
"require": ["tsconfig-paths/register"],
|
||||
"swc": true
|
||||
},
|
||||
"include": ["src/**/*", "package.json"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit c4c98ae70f2e936009250893898ecf11f4ddf2c3
|
||||
Subproject commit 65ac3fc8a20a5244fbe47629cf79678db2d9ae8a
|
|
@ -41,17 +41,9 @@ COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.
|
|||
RUN chmod +x ./scripts/removeWorkspaceDependencies.sh
|
||||
|
||||
|
||||
WORKDIR /string-templates
|
||||
COPY packages/string-templates/package.json package.json
|
||||
RUN ../scripts/removeWorkspaceDependencies.sh package.json
|
||||
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true --network-timeout 1000000
|
||||
COPY packages/string-templates .
|
||||
|
||||
|
||||
WORKDIR /app
|
||||
COPY packages/server/package.json .
|
||||
COPY packages/server/dist/yarn.lock .
|
||||
RUN cd ../string-templates && yarn link && cd - && yarn link @budibase/string-templates
|
||||
|
||||
COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.sh
|
||||
RUN chmod +x ./scripts/removeWorkspaceDependencies.sh
|
||||
|
|
|
@ -30,6 +30,8 @@ const baseConfig: Config.InitialProjectOptions = {
|
|||
"@budibase/backend-core": "<rootDir>/../backend-core/src",
|
||||
"@budibase/shared-core": "<rootDir>/../shared-core/src",
|
||||
"@budibase/types": "<rootDir>/../types/src",
|
||||
"@budibase/string-templates/(.*)": ["<rootDir>/../string-templates/$1"],
|
||||
"@budibase/string-templates": ["<rootDir>/../string-templates/src"],
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
ViewV2,
|
||||
} from "@budibase/types"
|
||||
import * as setup from "./utilities"
|
||||
import { mocks } from "@budibase/backend-core/tests"
|
||||
import { generator, mocks } from "@budibase/backend-core/tests"
|
||||
|
||||
const { basicRow } = setup.structures
|
||||
const { BUILTIN_ROLE_IDS } = roles
|
||||
|
@ -44,7 +44,10 @@ describe("/permission", () => {
|
|||
|
||||
table = (await config.createTable()) as typeof table
|
||||
row = await config.createRow()
|
||||
view = await config.api.viewV2.create({ tableId: table._id })
|
||||
view = await config.api.viewV2.create({
|
||||
tableId: table._id!,
|
||||
name: generator.guid(),
|
||||
})
|
||||
perms = await config.api.permission.add({
|
||||
roleId: STD_ROLE_ID,
|
||||
resourceId: table._id,
|
||||
|
|
|
@ -42,6 +42,7 @@ tk.freeze(timestamp)
|
|||
jest.unmock("mysql2")
|
||||
jest.unmock("mysql2/promise")
|
||||
jest.unmock("mssql")
|
||||
jest.unmock("pg")
|
||||
|
||||
describe.each([
|
||||
["internal", undefined],
|
||||
|
@ -152,8 +153,8 @@ describe.each([
|
|||
table = await config.api.table.save(defaultTable())
|
||||
})
|
||||
|
||||
describe("save, load, update", () => {
|
||||
it("returns a success message when the row is created", async () => {
|
||||
describe("create", () => {
|
||||
it("creates a new row successfully", async () => {
|
||||
const rowUsage = await getRowUsage()
|
||||
const row = await config.api.row.save(table._id!, {
|
||||
name: "Test Contact",
|
||||
|
@ -163,7 +164,44 @@ describe.each([
|
|||
await assertRowUsage(rowUsage + 1)
|
||||
})
|
||||
|
||||
it("Increment row autoId per create row request", async () => {
|
||||
it("fails to create a row for a table that does not exist", async () => {
|
||||
const rowUsage = await getRowUsage()
|
||||
await config.api.row.save("1234567", {}, { status: 404 })
|
||||
await assertRowUsage(rowUsage)
|
||||
})
|
||||
|
||||
it("fails to create a row if required fields are missing", async () => {
|
||||
const rowUsage = await getRowUsage()
|
||||
const table = await config.api.table.save(
|
||||
saveTableRequest({
|
||||
schema: {
|
||||
required: {
|
||||
type: FieldType.STRING,
|
||||
name: "required",
|
||||
constraints: {
|
||||
type: "string",
|
||||
presence: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
await config.api.row.save(
|
||||
table._id!,
|
||||
{},
|
||||
{
|
||||
status: 500,
|
||||
body: {
|
||||
validationErrors: {
|
||||
required: ["can't be blank"],
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
await assertRowUsage(rowUsage)
|
||||
})
|
||||
|
||||
it("increment row autoId per create row request", async () => {
|
||||
const rowUsage = await getRowUsage()
|
||||
|
||||
const newTable = await config.api.table.save(
|
||||
|
@ -198,52 +236,6 @@ describe.each([
|
|||
await assertRowUsage(rowUsage + 10)
|
||||
})
|
||||
|
||||
it("updates a row successfully", async () => {
|
||||
const existing = await config.api.row.save(table._id!, {})
|
||||
const rowUsage = await getRowUsage()
|
||||
|
||||
const res = await config.api.row.save(table._id!, {
|
||||
_id: existing._id,
|
||||
_rev: existing._rev,
|
||||
name: "Updated Name",
|
||||
})
|
||||
|
||||
expect(res.name).toEqual("Updated Name")
|
||||
await assertRowUsage(rowUsage)
|
||||
})
|
||||
|
||||
it("should load a row", async () => {
|
||||
const existing = await config.api.row.save(table._id!, {})
|
||||
|
||||
const res = await config.api.row.get(table._id!, existing._id!)
|
||||
|
||||
expect(res).toEqual({
|
||||
...existing,
|
||||
...defaultRowFields,
|
||||
})
|
||||
})
|
||||
|
||||
it("should list all rows for given tableId", async () => {
|
||||
const table = await config.api.table.save(defaultTable())
|
||||
const rows = await Promise.all([
|
||||
config.api.row.save(table._id!, {}),
|
||||
config.api.row.save(table._id!, {}),
|
||||
])
|
||||
|
||||
const res = await config.api.row.fetch(table._id!)
|
||||
expect(res.map(r => r._id)).toEqual(
|
||||
expect.arrayContaining(rows.map(r => r._id))
|
||||
)
|
||||
})
|
||||
|
||||
it("load should return 404 when row does not exist", async () => {
|
||||
const table = await config.api.table.save(defaultTable())
|
||||
await config.api.row.save(table._id!, {})
|
||||
await config.api.row.get(table._id!, "1234567", {
|
||||
status: 404,
|
||||
})
|
||||
})
|
||||
|
||||
isInternal &&
|
||||
it("row values are coerced", async () => {
|
||||
const str: FieldSchema = {
|
||||
|
@ -296,8 +288,6 @@ describe.each([
|
|||
}
|
||||
const table = await config.api.table.save(
|
||||
saveTableRequest({
|
||||
name: "TestTable2",
|
||||
type: "table",
|
||||
schema: {
|
||||
name: str,
|
||||
stringUndefined: str,
|
||||
|
@ -404,53 +394,60 @@ describe.each([
|
|||
})
|
||||
})
|
||||
|
||||
describe("view save", () => {
|
||||
it("views have extra data trimmed", async () => {
|
||||
const table = await config.api.table.save(
|
||||
saveTableRequest({
|
||||
name: "orders",
|
||||
schema: {
|
||||
Country: {
|
||||
type: FieldType.STRING,
|
||||
name: "Country",
|
||||
},
|
||||
Story: {
|
||||
type: FieldType.STRING,
|
||||
name: "Story",
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
describe("get", () => {
|
||||
it("reads an existing row successfully", async () => {
|
||||
const existing = await config.api.row.save(table._id!, {})
|
||||
|
||||
const createViewResponse = await config.api.viewV2.create({
|
||||
tableId: table._id!,
|
||||
name: uuid.v4(),
|
||||
schema: {
|
||||
Country: {
|
||||
visible: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
const res = await config.api.row.get(table._id!, existing._id!)
|
||||
|
||||
const createRowResponse = await config.api.row.save(
|
||||
createViewResponse.id,
|
||||
{
|
||||
Country: "Aussy",
|
||||
Story: "aaaaa",
|
||||
}
|
||||
)
|
||||
|
||||
const row = await config.api.row.get(table._id!, createRowResponse._id!)
|
||||
expect(row.Story).toBeUndefined()
|
||||
expect(row).toEqual({
|
||||
expect(res).toEqual({
|
||||
...existing,
|
||||
...defaultRowFields,
|
||||
Country: "Aussy",
|
||||
id: createRowResponse.id,
|
||||
_id: createRowResponse._id,
|
||||
_rev: createRowResponse._rev,
|
||||
tableId: table._id,
|
||||
})
|
||||
})
|
||||
|
||||
it("returns 404 when row does not exist", async () => {
|
||||
const table = await config.api.table.save(defaultTable())
|
||||
await config.api.row.save(table._id!, {})
|
||||
await config.api.row.get(table._id!, "1234567", {
|
||||
status: 404,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("fetch", () => {
|
||||
it("fetches all rows for given tableId", async () => {
|
||||
const table = await config.api.table.save(defaultTable())
|
||||
const rows = await Promise.all([
|
||||
config.api.row.save(table._id!, {}),
|
||||
config.api.row.save(table._id!, {}),
|
||||
])
|
||||
|
||||
const res = await config.api.row.fetch(table._id!)
|
||||
expect(res.map(r => r._id)).toEqual(
|
||||
expect.arrayContaining(rows.map(r => r._id))
|
||||
)
|
||||
})
|
||||
|
||||
it("returns 404 when table does not exist", async () => {
|
||||
await config.api.row.fetch("1234567", { status: 404 })
|
||||
})
|
||||
})
|
||||
|
||||
describe("update", () => {
|
||||
it("updates an existing row successfully", async () => {
|
||||
const existing = await config.api.row.save(table._id!, {})
|
||||
const rowUsage = await getRowUsage()
|
||||
|
||||
const res = await config.api.row.save(table._id!, {
|
||||
_id: existing._id,
|
||||
_rev: existing._rev,
|
||||
name: "Updated Name",
|
||||
})
|
||||
|
||||
expect(res.name).toEqual("Updated Name")
|
||||
await assertRowUsage(rowUsage)
|
||||
})
|
||||
})
|
||||
|
||||
describe("patch", () => {
|
||||
|
@ -722,50 +719,7 @@ describe.each([
|
|||
})
|
||||
})
|
||||
|
||||
// Legacy views are not available for external
|
||||
isInternal &&
|
||||
describe("fetchView", () => {
|
||||
beforeEach(async () => {
|
||||
table = await config.api.table.save(defaultTable())
|
||||
})
|
||||
|
||||
it("should be able to fetch tables contents via 'view'", async () => {
|
||||
const row = await config.api.row.save(table._id!, {})
|
||||
const rowUsage = await getRowUsage()
|
||||
|
||||
const rows = await config.api.legacyView.get(table._id!)
|
||||
expect(rows.length).toEqual(1)
|
||||
expect(rows[0]._id).toEqual(row._id)
|
||||
await assertRowUsage(rowUsage)
|
||||
})
|
||||
|
||||
it("should throw an error if view doesn't exist", async () => {
|
||||
const rowUsage = await getRowUsage()
|
||||
|
||||
await config.api.legacyView.get("derp", undefined, { status: 404 })
|
||||
|
||||
await assertRowUsage(rowUsage)
|
||||
})
|
||||
|
||||
it("should be able to run on a view", async () => {
|
||||
const view = await config.createLegacyView({
|
||||
tableId: table._id!,
|
||||
name: "ViewTest",
|
||||
filters: [],
|
||||
schema: {},
|
||||
})
|
||||
const row = await config.api.row.save(table._id!, {})
|
||||
const rowUsage = await getRowUsage()
|
||||
|
||||
const rows = await config.api.legacyView.get(view.name)
|
||||
expect(rows.length).toEqual(1)
|
||||
expect(rows[0]._id).toEqual(row._id)
|
||||
|
||||
await assertRowUsage(rowUsage)
|
||||
})
|
||||
})
|
||||
|
||||
describe("fetchEnrichedRows", () => {
|
||||
describe("enrich", () => {
|
||||
beforeAll(async () => {
|
||||
table = await config.api.table.save(defaultTable())
|
||||
})
|
||||
|
@ -827,10 +781,6 @@ describe.each([
|
|||
|
||||
isInternal &&
|
||||
describe("attachments", () => {
|
||||
beforeAll(async () => {
|
||||
table = await config.api.table.save(defaultTable())
|
||||
})
|
||||
|
||||
it("should allow enriching attachment rows", async () => {
|
||||
const table = await config.api.table.save(
|
||||
defaultTable({
|
||||
|
@ -865,7 +815,7 @@ describe.each([
|
|||
})
|
||||
})
|
||||
|
||||
describe("exportData", () => {
|
||||
describe("exportRows", () => {
|
||||
beforeAll(async () => {
|
||||
table = await config.api.table.save(defaultTable())
|
||||
})
|
||||
|
@ -947,6 +897,7 @@ describe.each([
|
|||
const table = await config.api.table.save(await userTable())
|
||||
const view = await config.api.viewV2.create({
|
||||
tableId: table._id!,
|
||||
name: generator.guid(),
|
||||
schema: {
|
||||
name: { visible: true },
|
||||
surname: { visible: true },
|
||||
|
@ -984,6 +935,7 @@ describe.each([
|
|||
const tableId = table._id!
|
||||
const view = await config.api.viewV2.create({
|
||||
tableId: tableId,
|
||||
name: generator.guid(),
|
||||
schema: {
|
||||
name: { visible: true },
|
||||
address: { visible: true },
|
||||
|
@ -1026,6 +978,7 @@ describe.each([
|
|||
const tableId = table._id!
|
||||
const view = await config.api.viewV2.create({
|
||||
tableId: tableId,
|
||||
name: generator.guid(),
|
||||
schema: {
|
||||
name: { visible: true },
|
||||
address: { visible: true },
|
||||
|
@ -1049,6 +1002,7 @@ describe.each([
|
|||
const tableId = table._id!
|
||||
const view = await config.api.viewV2.create({
|
||||
tableId: tableId,
|
||||
name: generator.guid(),
|
||||
schema: {
|
||||
name: { visible: true },
|
||||
address: { visible: true },
|
||||
|
@ -1109,6 +1063,7 @@ describe.each([
|
|||
|
||||
const createViewResponse = await config.api.viewV2.create({
|
||||
tableId: table._id!,
|
||||
name: generator.guid(),
|
||||
})
|
||||
const response = await config.api.viewV2.search(createViewResponse.id)
|
||||
|
||||
|
@ -1155,6 +1110,7 @@ describe.each([
|
|||
|
||||
const createViewResponse = await config.api.viewV2.create({
|
||||
tableId: table._id!,
|
||||
name: generator.guid(),
|
||||
query: [
|
||||
{ operator: SearchQueryOperators.EQUAL, field: "age", value: 40 },
|
||||
],
|
||||
|
@ -1279,6 +1235,7 @@ describe.each([
|
|||
async (sortParams, expected) => {
|
||||
const createViewResponse = await config.api.viewV2.create({
|
||||
tableId: table._id!,
|
||||
name: generator.guid(),
|
||||
sort: sortParams,
|
||||
schema: viewSchema,
|
||||
})
|
||||
|
@ -1299,6 +1256,7 @@ describe.each([
|
|||
async (sortParams, expected) => {
|
||||
const createViewResponse = await config.api.viewV2.create({
|
||||
tableId: table._id!,
|
||||
name: generator.guid(),
|
||||
sort: {
|
||||
field: "name",
|
||||
order: SortOrder.ASCENDING,
|
||||
|
@ -1339,6 +1297,7 @@ describe.each([
|
|||
|
||||
const view = await config.api.viewV2.create({
|
||||
tableId: table._id!,
|
||||
name: generator.guid(),
|
||||
schema: { name: { visible: true } },
|
||||
})
|
||||
const response = await config.api.viewV2.search(view.id)
|
||||
|
@ -1361,6 +1320,7 @@ describe.each([
|
|||
const table = await config.api.table.save(await userTable())
|
||||
const createViewResponse = await config.api.viewV2.create({
|
||||
tableId: table._id!,
|
||||
name: generator.guid(),
|
||||
})
|
||||
const response = await config.api.viewV2.search(createViewResponse.id)
|
||||
expect(response.rows).toHaveLength(0)
|
||||
|
@ -1376,6 +1336,7 @@ describe.each([
|
|||
|
||||
const createViewResponse = await config.api.viewV2.create({
|
||||
tableId: table._id!,
|
||||
name: generator.guid(),
|
||||
})
|
||||
const response = await config.api.viewV2.search(createViewResponse.id, {
|
||||
limit,
|
||||
|
@ -1392,6 +1353,7 @@ describe.each([
|
|||
)
|
||||
const view = await config.api.viewV2.create({
|
||||
tableId: table._id!,
|
||||
name: generator.guid(),
|
||||
})
|
||||
const rows = (await config.api.viewV2.search(view.id)).rows
|
||||
|
||||
|
@ -1466,6 +1428,7 @@ describe.each([
|
|||
|
||||
view = await config.api.viewV2.create({
|
||||
tableId: table._id!,
|
||||
name: generator.guid(),
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ import sdk from "../../../sdk"
|
|||
import * as uuid from "uuid"
|
||||
|
||||
import tk from "timekeeper"
|
||||
import { mocks } from "@budibase/backend-core/tests"
|
||||
import { generator, mocks } from "@budibase/backend-core/tests"
|
||||
import { TableToBuild } from "../../../tests/utilities/TestConfiguration"
|
||||
|
||||
tk.freeze(mocks.date.MOCK_DATE)
|
||||
|
@ -417,8 +417,8 @@ describe("/tables", () => {
|
|||
it("should fetch views", async () => {
|
||||
const tableId = config.table!._id!
|
||||
const views = [
|
||||
await config.api.viewV2.create({ tableId }),
|
||||
await config.api.viewV2.create({ tableId }),
|
||||
await config.api.viewV2.create({ tableId, name: generator.guid() }),
|
||||
await config.api.viewV2.create({ tableId, name: generator.guid() }),
|
||||
]
|
||||
|
||||
const res = await request
|
||||
|
@ -455,7 +455,7 @@ describe("/tables", () => {
|
|||
},
|
||||
}))
|
||||
|
||||
await config.api.viewV2.create({ tableId })
|
||||
await config.api.viewV2.create({ tableId, name: generator.guid() })
|
||||
await config.createLegacyView()
|
||||
|
||||
const res = await config.api.table.fetch()
|
||||
|
|
|
@ -3,12 +3,15 @@ import * as setup from "./utilities"
|
|||
import {
|
||||
FieldType,
|
||||
INTERNAL_TABLE_SOURCE_ID,
|
||||
QuotaUsageType,
|
||||
SaveTableRequest,
|
||||
StaticQuotaName,
|
||||
Table,
|
||||
TableSourceType,
|
||||
View,
|
||||
ViewCalculation,
|
||||
} from "@budibase/types"
|
||||
import { quotas } from "@budibase/pro"
|
||||
|
||||
const priceTable: SaveTableRequest = {
|
||||
name: "table",
|
||||
|
@ -57,6 +60,18 @@ describe("/views", () => {
|
|||
return config.api.legacyView.save(viewToSave)
|
||||
}
|
||||
|
||||
const getRowUsage = async () => {
|
||||
const { total } = await config.doInContext(undefined, () =>
|
||||
quotas.getCurrentUsageValues(QuotaUsageType.STATIC, StaticQuotaName.ROWS)
|
||||
)
|
||||
return total
|
||||
}
|
||||
|
||||
const assertRowUsage = async (expected: number) => {
|
||||
const usage = await getRowUsage()
|
||||
expect(usage).toBe(expected)
|
||||
}
|
||||
|
||||
describe("create", () => {
|
||||
it("returns a success message when the view is successfully created", async () => {
|
||||
const res = await saveView()
|
||||
|
@ -265,6 +280,41 @@ describe("/views", () => {
|
|||
expect(views.length).toBe(1)
|
||||
expect(views.find(({ name }) => name === "TestView")).toBeDefined()
|
||||
})
|
||||
|
||||
it("should be able to fetch tables contents via 'view'", async () => {
|
||||
const row = await config.api.row.save(table._id!, {})
|
||||
const rowUsage = await getRowUsage()
|
||||
|
||||
const rows = await config.api.legacyView.get(table._id!)
|
||||
expect(rows.length).toEqual(1)
|
||||
expect(rows[0]._id).toEqual(row._id)
|
||||
await assertRowUsage(rowUsage)
|
||||
})
|
||||
|
||||
it("should throw an error if view doesn't exist", async () => {
|
||||
const rowUsage = await getRowUsage()
|
||||
|
||||
await config.api.legacyView.get("derp", undefined, { status: 404 })
|
||||
|
||||
await assertRowUsage(rowUsage)
|
||||
})
|
||||
|
||||
it("should be able to run on a view", async () => {
|
||||
const view = await config.api.legacyView.save({
|
||||
tableId: table._id!,
|
||||
name: "ViewTest",
|
||||
filters: [],
|
||||
schema: {},
|
||||
})
|
||||
const row = await config.api.row.save(table._id!, {})
|
||||
const rowUsage = await getRowUsage()
|
||||
|
||||
const rows = await config.api.legacyView.get(view.name!)
|
||||
expect(rows.length).toEqual(1)
|
||||
expect(rows[0]._id).toEqual(row._id)
|
||||
|
||||
await assertRowUsage(rowUsage)
|
||||
})
|
||||
})
|
||||
|
||||
describe("query", () => {
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import * as setup from "./utilities"
|
||||
import {
|
||||
CreateViewRequest,
|
||||
Datasource,
|
||||
FieldSchema,
|
||||
FieldType,
|
||||
INTERNAL_TABLE_SOURCE_ID,
|
||||
SaveTableRequest,
|
||||
SearchQueryOperators,
|
||||
SortOrder,
|
||||
SortType,
|
||||
|
@ -14,65 +16,88 @@ import {
|
|||
ViewV2,
|
||||
} from "@budibase/types"
|
||||
import { generator } from "@budibase/backend-core/tests"
|
||||
import { generateDatasourceID } from "../../../db/utils"
|
||||
import * as uuid from "uuid"
|
||||
import { databaseTestProviders } from "../../../integrations/tests/utils"
|
||||
import merge from "lodash/merge"
|
||||
|
||||
function priceTable(): Table {
|
||||
return {
|
||||
name: "table",
|
||||
type: "table",
|
||||
sourceId: INTERNAL_TABLE_SOURCE_ID,
|
||||
sourceType: TableSourceType.INTERNAL,
|
||||
schema: {
|
||||
Price: {
|
||||
type: FieldType.NUMBER,
|
||||
name: "Price",
|
||||
constraints: {},
|
||||
},
|
||||
Category: {
|
||||
type: FieldType.STRING,
|
||||
name: "Category",
|
||||
constraints: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const config = setup.getConfig()
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
})
|
||||
jest.unmock("mysql2")
|
||||
jest.unmock("mysql2/promise")
|
||||
jest.unmock("mssql")
|
||||
jest.unmock("pg")
|
||||
|
||||
describe.each([
|
||||
["internal ds", () => config.createTable(priceTable())],
|
||||
[
|
||||
"external ds",
|
||||
async () => {
|
||||
const datasource = await config.createDatasource({
|
||||
datasource: {
|
||||
...setup.structures.basicDatasource().datasource,
|
||||
plus: true,
|
||||
_id: generateDatasourceID({ plus: true }),
|
||||
},
|
||||
})
|
||||
["internal", undefined],
|
||||
["postgres", databaseTestProviders.postgres],
|
||||
["mysql", databaseTestProviders.mysql],
|
||||
["mssql", databaseTestProviders.mssql],
|
||||
["mariadb", databaseTestProviders.mariadb],
|
||||
])("/v2/views (%s)", (_, dsProvider) => {
|
||||
const config = setup.getConfig()
|
||||
|
||||
return config.createExternalTable({
|
||||
...priceTable(),
|
||||
sourceId: datasource._id,
|
||||
sourceType: TableSourceType.EXTERNAL,
|
||||
})
|
||||
},
|
||||
],
|
||||
])("/v2/views (%s)", (_, tableBuilder) => {
|
||||
let table: Table
|
||||
let datasource: Datasource
|
||||
|
||||
function saveTableRequest(
|
||||
...overrides: Partial<SaveTableRequest>[]
|
||||
): SaveTableRequest {
|
||||
const req: SaveTableRequest = {
|
||||
name: uuid.v4().substring(0, 16),
|
||||
type: "table",
|
||||
sourceType: datasource
|
||||
? TableSourceType.EXTERNAL
|
||||
: TableSourceType.INTERNAL,
|
||||
sourceId: datasource ? datasource._id! : INTERNAL_TABLE_SOURCE_ID,
|
||||
primary: ["id"],
|
||||
schema: {
|
||||
id: {
|
||||
type: FieldType.AUTO,
|
||||
name: "id",
|
||||
autocolumn: true,
|
||||
constraints: {
|
||||
presence: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
return merge(req, ...overrides)
|
||||
}
|
||||
|
||||
function priceTable(): SaveTableRequest {
|
||||
return saveTableRequest({
|
||||
schema: {
|
||||
Price: {
|
||||
type: FieldType.NUMBER,
|
||||
name: "Price",
|
||||
constraints: {},
|
||||
},
|
||||
Category: {
|
||||
type: FieldType.STRING,
|
||||
name: "Category",
|
||||
constraints: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
table = await tableBuilder()
|
||||
await config.init()
|
||||
|
||||
if (dsProvider) {
|
||||
datasource = await config.createDatasource({
|
||||
datasource: await dsProvider.datasource(),
|
||||
})
|
||||
}
|
||||
table = await config.api.table.save(priceTable())
|
||||
})
|
||||
|
||||
afterAll(setup.afterAll)
|
||||
afterAll(async () => {
|
||||
if (dsProvider) {
|
||||
await dsProvider.stop()
|
||||
}
|
||||
setup.afterAll()
|
||||
})
|
||||
|
||||
describe("create", () => {
|
||||
it("persist the view when the view is successfully created", async () => {
|
||||
|
@ -186,9 +211,12 @@ describe.each([
|
|||
let view: ViewV2
|
||||
|
||||
beforeEach(async () => {
|
||||
table = await tableBuilder()
|
||||
table = await config.api.table.save(priceTable())
|
||||
|
||||
view = await config.api.viewV2.create({ name: "View A" })
|
||||
view = await config.api.viewV2.create({
|
||||
tableId: table._id!,
|
||||
name: "View A",
|
||||
})
|
||||
})
|
||||
|
||||
it("can update an existing view data", async () => {
|
||||
|
@ -247,6 +275,9 @@ describe.each([
|
|||
...updatedData,
|
||||
schema: {
|
||||
...table.schema,
|
||||
id: expect.objectContaining({
|
||||
visible: false,
|
||||
}),
|
||||
Category: expect.objectContaining({
|
||||
visible: false,
|
||||
}),
|
||||
|
@ -320,23 +351,27 @@ describe.each([
|
|||
})
|
||||
|
||||
it("cannot update views v1", async () => {
|
||||
const viewV1 = await config.createLegacyView()
|
||||
await config.api.viewV2.update(
|
||||
{
|
||||
...viewV1,
|
||||
},
|
||||
{
|
||||
const viewV1 = await config.api.legacyView.save({
|
||||
tableId: table._id!,
|
||||
name: generator.guid(),
|
||||
filters: [],
|
||||
schema: {},
|
||||
})
|
||||
|
||||
await config.api.viewV2.update(viewV1 as unknown as ViewV2, {
|
||||
status: 400,
|
||||
body: {
|
||||
message: "Only views V2 can be updated",
|
||||
status: 400,
|
||||
body: {
|
||||
message: "Only views V2 can be updated",
|
||||
status: 400,
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("cannot update the a view with unmatching ids between url and body", async () => {
|
||||
const anotherView = await config.api.viewV2.create()
|
||||
const anotherView = await config.api.viewV2.create({
|
||||
tableId: table._id!,
|
||||
name: generator.guid(),
|
||||
})
|
||||
const result = await config
|
||||
.request!.put(`/api/v2/views/${anotherView.id}`)
|
||||
.send(view)
|
||||
|
@ -411,7 +446,10 @@ describe.each([
|
|||
let view: ViewV2
|
||||
|
||||
beforeAll(async () => {
|
||||
view = await config.api.viewV2.create()
|
||||
view = await config.api.viewV2.create({
|
||||
tableId: table._id!,
|
||||
name: generator.guid(),
|
||||
})
|
||||
})
|
||||
|
||||
it("can delete an existing view", async () => {
|
||||
|
@ -448,4 +486,43 @@ describe.each([
|
|||
expect(viewSchema.Price?.visible).toEqual(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("read", () => {
|
||||
it("views have extra data trimmed", async () => {
|
||||
const table = await config.api.table.save(
|
||||
saveTableRequest({
|
||||
name: "orders",
|
||||
schema: {
|
||||
Country: {
|
||||
type: FieldType.STRING,
|
||||
name: "Country",
|
||||
},
|
||||
Story: {
|
||||
type: FieldType.STRING,
|
||||
name: "Story",
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
const view = await config.api.viewV2.create({
|
||||
tableId: table._id!,
|
||||
name: uuid.v4(),
|
||||
schema: {
|
||||
Country: {
|
||||
visible: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
let row = await config.api.row.save(view.id, {
|
||||
Country: "Aussy",
|
||||
Story: "aaaaa",
|
||||
})
|
||||
|
||||
row = await config.api.row.get(table._id!, row._id!)
|
||||
expect(row.Story).toBeUndefined()
|
||||
expect(row.Country).toEqual("Aussy")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { validate as isValidUUID } from "uuid"
|
||||
import { processStringSync, encodeJSBinding } from "@budibase/string-templates"
|
||||
|
||||
const { runJsHelpersTests } = require("@budibase/string-templates/test/utils")
|
||||
import { runJsHelpersTests } from "@budibase/string-templates/test/utils"
|
||||
|
||||
import tk from "timekeeper"
|
||||
import { init } from ".."
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
import * as search from "../../app/rows/search"
|
||||
|
||||
describe("removeEmptyFilters", () => {
|
||||
it("0 should not be removed", () => {
|
||||
const filters = search.removeEmptyFilters({
|
||||
equal: {
|
||||
column: 0,
|
||||
},
|
||||
})
|
||||
expect((filters.equal as any).column).toBe(0)
|
||||
})
|
||||
|
||||
it("empty string should be removed", () => {
|
||||
const filters = search.removeEmptyFilters({
|
||||
equal: {
|
||||
column: "",
|
||||
},
|
||||
})
|
||||
expect(Object.values(filters.equal as any).length).toBe(0)
|
||||
})
|
||||
})
|
|
@ -11,21 +11,9 @@ import sdk from "../../../sdk"
|
|||
|
||||
export class ViewV2API extends TestAPI {
|
||||
create = async (
|
||||
viewData?: Partial<CreateViewRequest>,
|
||||
view: CreateViewRequest,
|
||||
expectations?: Expectations
|
||||
): Promise<ViewV2> => {
|
||||
let tableId = viewData?.tableId
|
||||
if (!tableId && !this.config.table) {
|
||||
throw "Test requires table to be configured."
|
||||
}
|
||||
|
||||
tableId = tableId || this.config.table!._id!
|
||||
const view = {
|
||||
tableId,
|
||||
name: generator.guid(),
|
||||
...viewData,
|
||||
}
|
||||
|
||||
const exp: Expectations = {
|
||||
status: 201,
|
||||
...expectations,
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: "ts-jest",
|
||||
// All imported modules in your tests should be mocked automatically
|
||||
// automock: false,
|
||||
|
||||
|
|
|
@ -2,29 +2,28 @@
|
|||
"name": "@budibase/string-templates",
|
||||
"version": "0.0.0",
|
||||
"description": "Handlebars wrapper for Budibase templating.",
|
||||
"main": "src/index.js",
|
||||
"main": "dist/bundle.cjs",
|
||||
"module": "dist/bundle.mjs",
|
||||
"types": "src/index.ts",
|
||||
"license": "MPL-2.0",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"require": "./src/index.js",
|
||||
"require": "./dist/bundle.cjs",
|
||||
"import": "./dist/bundle.mjs"
|
||||
},
|
||||
"./package.json": "./package.json",
|
||||
"./test/utils": "./test/utils.js",
|
||||
"./iife": "./src/iife.js"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"src",
|
||||
"manifest.json"
|
||||
"src"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc && rollup -c",
|
||||
"dev": "concurrently \"tsc --watch\" \"rollup -cw\"",
|
||||
"build": "tsc --emitDeclarationOnly && rollup -c",
|
||||
"dev": "rollup -cw",
|
||||
"check:types": "tsc -p tsconfig.json --noEmit --paths null",
|
||||
"test": "jest",
|
||||
"manifest": "node ./scripts/gen-collection-info.js"
|
||||
"manifest": "ts-node ./scripts/gen-collection-info.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/handlebars-helpers": "^0.13.1",
|
||||
|
@ -34,8 +33,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^17.1.0",
|
||||
"@rollup/plugin-json": "^4.1.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"@rollup/plugin-typescript": "8.3.0",
|
||||
"doctrine": "^3.0.0",
|
||||
"jest": "29.7.0",
|
||||
"marked": "^4.0.10",
|
||||
|
|
|
@ -4,31 +4,36 @@ import json from "@rollup/plugin-json"
|
|||
import { terser } from "rollup-plugin-terser"
|
||||
import builtins from "rollup-plugin-node-builtins"
|
||||
import globals from "rollup-plugin-node-globals"
|
||||
import typescript from "@rollup/plugin-typescript"
|
||||
import injectProcessEnv from "rollup-plugin-inject-process-env"
|
||||
|
||||
const production = !process.env.ROLLUP_WATCH
|
||||
|
||||
export default [
|
||||
{
|
||||
input: "src/index.mjs",
|
||||
output: {
|
||||
sourcemap: !production,
|
||||
format: "esm",
|
||||
file: "./dist/bundle.mjs",
|
||||
},
|
||||
plugins: [
|
||||
resolve({
|
||||
preferBuiltins: true,
|
||||
browser: true,
|
||||
}),
|
||||
commonjs(),
|
||||
globals(),
|
||||
builtins(),
|
||||
json(),
|
||||
injectProcessEnv({
|
||||
NO_JS: process.env.NO_JS,
|
||||
}),
|
||||
production && terser(),
|
||||
],
|
||||
const config = (format, outputFile) => ({
|
||||
input: "src/index.ts",
|
||||
output: {
|
||||
sourcemap: !production,
|
||||
format,
|
||||
file: outputFile,
|
||||
},
|
||||
plugins: [
|
||||
typescript(),
|
||||
resolve({
|
||||
preferBuiltins: true,
|
||||
browser: true,
|
||||
}),
|
||||
commonjs(),
|
||||
globals(),
|
||||
builtins(),
|
||||
json(),
|
||||
injectProcessEnv({
|
||||
NO_JS: process.env.NO_JS,
|
||||
}),
|
||||
production && terser(),
|
||||
],
|
||||
})
|
||||
|
||||
export default [
|
||||
config("cjs", "./dist/bundle.cjs"),
|
||||
config("esm", "./dist/bundle.mjs"),
|
||||
]
|
||||
|
|
|
@ -22,7 +22,7 @@ const COLLECTIONS = [
|
|||
"object",
|
||||
"uuid",
|
||||
]
|
||||
const FILENAME = join(__dirname, "..", "manifest.json")
|
||||
const FILENAME = join(__dirname, "..", "src", "manifest.json")
|
||||
const outputJSON = {}
|
||||
const ADDED_HELPERS = {
|
||||
date: {
|
||||
|
@ -126,7 +126,7 @@ const excludeFunctions = { string: ["raw"] }
|
|||
* This script is very specific to purpose, parsing the handlebars-helpers files to attempt to get information about them.
|
||||
*/
|
||||
function run() {
|
||||
const foundNames = []
|
||||
const foundNames: string[] = []
|
||||
for (let collection of COLLECTIONS) {
|
||||
const collectionFile = fs.readFileSync(
|
||||
`${path.dirname(require.resolve(HELPER_LIBRARY))}/lib/${collection}.js`,
|
||||
|
@ -147,7 +147,7 @@ function run() {
|
|||
}
|
||||
foundNames.push(name)
|
||||
// this is ridiculous, but it parse the function header
|
||||
const fnc = entry[1].toString()
|
||||
const fnc = entry[1]!.toString()
|
||||
const jsDocInfo = getCommentInfo(collectionFile, fnc)
|
||||
let args = jsDocInfo.tags
|
||||
.filter(tag => tag.title === "param")
|
||||
|
@ -176,8 +176,8 @@ function run() {
|
|||
}
|
||||
|
||||
// convert all markdown to HTML
|
||||
for (let collection of Object.values(outputJSON)) {
|
||||
for (let helper of Object.values(collection)) {
|
||||
for (let collection of Object.values<any>(outputJSON)) {
|
||||
for (let helper of Object.values<any>(collection)) {
|
||||
helper.description = marked.parse(helper.description)
|
||||
}
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
const { getJsHelperList } = require("../helpers")
|
||||
import { getJsHelperList } from "../helpers"
|
||||
|
||||
function getLayers(fullBlock) {
|
||||
function getLayers(fullBlock: string): string[] {
|
||||
let layers = []
|
||||
while (fullBlock.length) {
|
||||
const start = fullBlock.lastIndexOf("("),
|
||||
end = fullBlock.indexOf(")")
|
||||
let layer
|
||||
let layer: string
|
||||
if (start === -1 || end === -1) {
|
||||
layer = fullBlock.trim()
|
||||
fullBlock = ""
|
||||
|
@ -21,7 +21,7 @@ function getLayers(fullBlock) {
|
|||
return layers
|
||||
}
|
||||
|
||||
function getVariable(variableName) {
|
||||
function getVariable(variableName: string) {
|
||||
if (!variableName || typeof variableName !== "string") {
|
||||
return variableName
|
||||
}
|
||||
|
@ -47,10 +47,12 @@ function getVariable(variableName) {
|
|||
return `$("${variableName}")`
|
||||
}
|
||||
|
||||
function buildList(parts, value) {
|
||||
function buildList(parts: string[], value: any) {
|
||||
function build() {
|
||||
return parts
|
||||
.map(part => (part.startsWith("helper") ? part : getVariable(part)))
|
||||
.map((part: string) =>
|
||||
part.startsWith("helper") ? part : getVariable(part)
|
||||
)
|
||||
.join(", ")
|
||||
}
|
||||
if (!value) {
|
||||
|
@ -60,12 +62,12 @@ function buildList(parts, value) {
|
|||
}
|
||||
}
|
||||
|
||||
function splitBySpace(layer) {
|
||||
const parts = []
|
||||
function splitBySpace(layer: string) {
|
||||
const parts: string[] = []
|
||||
let started = null,
|
||||
endChar = null,
|
||||
last = 0
|
||||
function add(str) {
|
||||
function add(str: string) {
|
||||
const startsWith = ["]"]
|
||||
while (startsWith.indexOf(str.substring(0, 1)) !== -1) {
|
||||
str = str.substring(1, str.length)
|
||||
|
@ -103,7 +105,7 @@ function splitBySpace(layer) {
|
|||
return parts
|
||||
}
|
||||
|
||||
module.exports.convertHBSBlock = (block, blockNumber) => {
|
||||
export function convertHBSBlock(block: string, blockNumber: number) {
|
||||
const braceLength = block[2] === "{" ? 3 : 2
|
||||
block = block.substring(braceLength, block.length - braceLength).trim()
|
||||
const layers = getLayers(block)
|
||||
|
@ -114,7 +116,7 @@ module.exports.convertHBSBlock = (block, blockNumber) => {
|
|||
const parts = splitBySpace(layer)
|
||||
if (value || parts.length > 1 || list[parts[0]]) {
|
||||
// first of layer should always be the helper
|
||||
const helper = parts.splice(0, 1)
|
||||
const [helper] = parts.splice(0, 1)
|
||||
if (list[helper]) {
|
||||
value = `helpers.${helper}(${buildList(parts, value)})`
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
class JsErrorTimeout extends Error {
|
||||
code = "ERR_SCRIPT_EXECUTION_TIMEOUT"
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
JsErrorTimeout,
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export class JsErrorTimeout extends Error {
|
||||
code = "ERR_SCRIPT_EXECUTION_TIMEOUT"
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
class Helper {
|
||||
constructor(name, fn, useValueFallback = true) {
|
||||
this.name = name
|
||||
this.fn = fn
|
||||
this.useValueFallback = useValueFallback
|
||||
}
|
||||
|
||||
register(handlebars) {
|
||||
// wrap the function so that no helper can cause handlebars to break
|
||||
handlebars.registerHelper(this.name, (value, info) => {
|
||||
let context = {}
|
||||
if (info && info.data && info.data.root) {
|
||||
context = info.data.root
|
||||
}
|
||||
const result = this.fn(value, context)
|
||||
if (result == null) {
|
||||
return this.useValueFallback ? value : null
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
unregister(handlebars) {
|
||||
handlebars.unregisterHelper(this.name)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Helper
|
|
@ -0,0 +1,34 @@
|
|||
export default class Helper {
|
||||
private name: any
|
||||
private fn: any
|
||||
private useValueFallback: boolean
|
||||
|
||||
constructor(name: string, fn: any, useValueFallback = true) {
|
||||
this.name = name
|
||||
this.fn = fn
|
||||
this.useValueFallback = useValueFallback
|
||||
}
|
||||
|
||||
register(handlebars: typeof Handlebars) {
|
||||
// wrap the function so that no helper can cause handlebars to break
|
||||
handlebars.registerHelper(
|
||||
this.name,
|
||||
(value: any, info: { data: { root: {} } }) => {
|
||||
let context = {}
|
||||
if (info && info.data && info.data.root) {
|
||||
context = info.data.root
|
||||
}
|
||||
const result = this.fn(value, context)
|
||||
if (result == null) {
|
||||
return this.useValueFallback ? value : null
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
unregister(handlebars: { unregisterHelper: any }) {
|
||||
handlebars.unregisterHelper(this.name)
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
module.exports.HelperFunctionBuiltin = [
|
||||
export const HelperFunctionBuiltin = [
|
||||
"#if",
|
||||
"#unless",
|
||||
"#each",
|
||||
|
@ -15,11 +15,11 @@ module.exports.HelperFunctionBuiltin = [
|
|||
"with",
|
||||
]
|
||||
|
||||
module.exports.HelperFunctionNames = {
|
||||
export const HelperFunctionNames = {
|
||||
OBJECT: "object",
|
||||
ALL: "all",
|
||||
LITERAL: "literal",
|
||||
JS: "js",
|
||||
}
|
||||
|
||||
module.exports.LITERAL_MARKER = "%LITERAL%"
|
||||
export const LITERAL_MARKER = "%LITERAL%"
|
|
@ -1,12 +1,22 @@
|
|||
const dayjs = require("dayjs")
|
||||
dayjs.extend(require("dayjs/plugin/duration"))
|
||||
dayjs.extend(require("dayjs/plugin/advancedFormat"))
|
||||
dayjs.extend(require("dayjs/plugin/isoWeek"))
|
||||
dayjs.extend(require("dayjs/plugin/weekYear"))
|
||||
dayjs.extend(require("dayjs/plugin/weekOfYear"))
|
||||
dayjs.extend(require("dayjs/plugin/relativeTime"))
|
||||
dayjs.extend(require("dayjs/plugin/utc"))
|
||||
dayjs.extend(require("dayjs/plugin/timezone"))
|
||||
import dayjs from "dayjs"
|
||||
|
||||
import dayjsDurationPlugin from "dayjs/plugin/duration"
|
||||
import dayjsAdvancedFormatPlugin from "dayjs/plugin/advancedFormat"
|
||||
import dayjsIsoWeekPlugin from "dayjs/plugin/isoWeek"
|
||||
import dayjsWeekYearPlugin from "dayjs/plugin/weekYear"
|
||||
import dayjsWeekOfYearPlugin from "dayjs/plugin/weekOfYear"
|
||||
import dayjsRelativeTimePlugin from "dayjs/plugin/relativeTime"
|
||||
import dayjsUtcPlugin from "dayjs/plugin/utc"
|
||||
import dayjsTimezonePlugin from "dayjs/plugin/timezone"
|
||||
|
||||
dayjs.extend(dayjsDurationPlugin)
|
||||
dayjs.extend(dayjsAdvancedFormatPlugin)
|
||||
dayjs.extend(dayjsIsoWeekPlugin)
|
||||
dayjs.extend(dayjsWeekYearPlugin)
|
||||
dayjs.extend(dayjsWeekOfYearPlugin)
|
||||
dayjs.extend(dayjsRelativeTimePlugin)
|
||||
dayjs.extend(dayjsUtcPlugin)
|
||||
dayjs.extend(dayjsTimezonePlugin)
|
||||
|
||||
/**
|
||||
* This file was largely taken from the helper-date package - we did this for two reasons:
|
||||
|
@ -17,11 +27,11 @@ dayjs.extend(require("dayjs/plugin/timezone"))
|
|||
* https://github.com/helpers/helper-date
|
||||
*/
|
||||
|
||||
function isOptions(val) {
|
||||
function isOptions(val: any) {
|
||||
return typeof val === "object" && typeof val.hash === "object"
|
||||
}
|
||||
|
||||
function isApp(thisArg) {
|
||||
function isApp(thisArg: any) {
|
||||
return (
|
||||
typeof thisArg === "object" &&
|
||||
typeof thisArg.options === "object" &&
|
||||
|
@ -29,7 +39,7 @@ function isApp(thisArg) {
|
|||
)
|
||||
}
|
||||
|
||||
function getContext(thisArg, locals, options) {
|
||||
function getContext(thisArg: any, locals: any, options: any) {
|
||||
if (isOptions(thisArg)) {
|
||||
return getContext({}, locals, thisArg)
|
||||
}
|
||||
|
@ -58,7 +68,7 @@ function getContext(thisArg, locals, options) {
|
|||
return context
|
||||
}
|
||||
|
||||
function initialConfig(str, pattern, options) {
|
||||
function initialConfig(str: any, pattern: any, options?: any) {
|
||||
if (isOptions(pattern)) {
|
||||
options = pattern
|
||||
pattern = null
|
||||
|
@ -72,7 +82,7 @@ function initialConfig(str, pattern, options) {
|
|||
return { str, pattern, options }
|
||||
}
|
||||
|
||||
function setLocale(str, pattern, options) {
|
||||
function setLocale(this: any, str: any, pattern: any, options?: any) {
|
||||
// if options is null then it'll get updated here
|
||||
const config = initialConfig(str, pattern, options)
|
||||
const defaults = { lang: "en", date: new Date(config.str) }
|
||||
|
@ -83,7 +93,7 @@ function setLocale(str, pattern, options) {
|
|||
dayjs.locale(opts.lang || opts.language)
|
||||
}
|
||||
|
||||
module.exports.date = (str, pattern, options) => {
|
||||
export const date = (str: any, pattern: any, options: any) => {
|
||||
const config = initialConfig(str, pattern, options)
|
||||
|
||||
// if no args are passed, return a formatted date
|
||||
|
@ -109,7 +119,7 @@ module.exports.date = (str, pattern, options) => {
|
|||
return date.format(config.pattern)
|
||||
}
|
||||
|
||||
module.exports.duration = (str, pattern, format) => {
|
||||
export const duration = (str: any, pattern: any, format: any) => {
|
||||
const config = initialConfig(str, pattern)
|
||||
|
||||
setLocale(config.str, config.pattern)
|
|
@ -1,6 +1,8 @@
|
|||
const helpers = require("@budibase/handlebars-helpers")
|
||||
const { date, duration } = require("./date")
|
||||
const { HelperFunctionBuiltin } = require("./constants")
|
||||
// @ts-ignore we don't have types for it
|
||||
import helpers from "@budibase/handlebars-helpers"
|
||||
|
||||
import { date, duration } from "./date"
|
||||
import { HelperFunctionBuiltin } from "./constants"
|
||||
|
||||
/**
|
||||
* full list of supported helpers can be found here:
|
||||
|
@ -24,10 +26,10 @@ const ADDED_HELPERS = {
|
|||
duration: duration,
|
||||
}
|
||||
|
||||
exports.externalCollections = EXTERNAL_FUNCTION_COLLECTIONS
|
||||
exports.addedHelpers = ADDED_HELPERS
|
||||
export const externalCollections = EXTERNAL_FUNCTION_COLLECTIONS
|
||||
export const addedHelpers = ADDED_HELPERS
|
||||
|
||||
exports.registerAll = handlebars => {
|
||||
export function registerAll(handlebars: typeof Handlebars) {
|
||||
for (let [name, helper] of Object.entries(ADDED_HELPERS)) {
|
||||
handlebars.registerHelper(name, helper)
|
||||
}
|
||||
|
@ -52,17 +54,17 @@ exports.registerAll = handlebars => {
|
|||
})
|
||||
}
|
||||
// add date external functionality
|
||||
exports.externalHelperNames = externalNames.concat(Object.keys(ADDED_HELPERS))
|
||||
externalHelperNames = externalNames.concat(Object.keys(ADDED_HELPERS))
|
||||
}
|
||||
|
||||
exports.unregisterAll = handlebars => {
|
||||
export function unregisterAll(handlebars: typeof Handlebars) {
|
||||
for (let name of Object.keys(ADDED_HELPERS)) {
|
||||
handlebars.unregisterHelper(name)
|
||||
}
|
||||
for (let name of module.exports.externalHelperNames) {
|
||||
for (let name of externalHelperNames) {
|
||||
handlebars.unregisterHelper(name)
|
||||
}
|
||||
exports.externalHelperNames = []
|
||||
externalHelperNames = []
|
||||
}
|
||||
|
||||
exports.externalHelperNames = []
|
||||
export let externalHelperNames: any[] = []
|
|
@ -1,100 +0,0 @@
|
|||
const Helper = require("./Helper")
|
||||
const { SafeString } = require("handlebars")
|
||||
const externalHandlebars = require("./external")
|
||||
const { processJS } = require("./javascript")
|
||||
const {
|
||||
HelperFunctionNames,
|
||||
HelperFunctionBuiltin,
|
||||
LITERAL_MARKER,
|
||||
} = require("./constants")
|
||||
const { getJsHelperList } = require("./list")
|
||||
|
||||
const HTML_SWAPS = {
|
||||
"<": "<",
|
||||
">": ">",
|
||||
}
|
||||
|
||||
function isObject(value) {
|
||||
if (value == null || typeof value !== "object") {
|
||||
return false
|
||||
}
|
||||
return (
|
||||
value.toString() === "[object Object]" ||
|
||||
(value.length > 0 && typeof value[0] === "object")
|
||||
)
|
||||
}
|
||||
|
||||
const HELPERS = [
|
||||
// external helpers
|
||||
new Helper(HelperFunctionNames.OBJECT, value => {
|
||||
return new SafeString(JSON.stringify(value))
|
||||
}),
|
||||
// javascript helper
|
||||
new Helper(HelperFunctionNames.JS, processJS, false),
|
||||
// this help is applied to all statements
|
||||
new Helper(HelperFunctionNames.ALL, (value, inputs) => {
|
||||
const { __opts } = inputs
|
||||
if (isObject(value)) {
|
||||
return new SafeString(JSON.stringify(value))
|
||||
}
|
||||
// null/undefined values produce bad results
|
||||
if (__opts && __opts.onlyFound && value == null) {
|
||||
return __opts.input
|
||||
}
|
||||
if (value == null || typeof value !== "string") {
|
||||
return value == null ? "" : value
|
||||
}
|
||||
if (value && value.string) {
|
||||
value = value.string
|
||||
}
|
||||
let text = value
|
||||
if (__opts && __opts.escapeNewlines) {
|
||||
text = value.replace(/\n/g, "\\n")
|
||||
}
|
||||
text = new SafeString(text.replace(/&/g, "&"))
|
||||
if (text == null || typeof text !== "string") {
|
||||
return text
|
||||
}
|
||||
return text.replace(/[<>]/g, tag => {
|
||||
return HTML_SWAPS[tag] || tag
|
||||
})
|
||||
}),
|
||||
// adds a note for post-processor
|
||||
new Helper(HelperFunctionNames.LITERAL, value => {
|
||||
if (value === undefined) {
|
||||
return ""
|
||||
}
|
||||
const type = typeof value
|
||||
const outputVal = type === "object" ? JSON.stringify(value) : value
|
||||
return `{{${LITERAL_MARKER} ${type}-${outputVal}}}`
|
||||
}),
|
||||
]
|
||||
|
||||
module.exports.HelperNames = () => {
|
||||
return Object.values(HelperFunctionNames).concat(
|
||||
HelperFunctionBuiltin,
|
||||
externalHandlebars.externalHelperNames
|
||||
)
|
||||
}
|
||||
|
||||
module.exports.registerMinimum = handlebars => {
|
||||
for (let helper of HELPERS) {
|
||||
helper.register(handlebars)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.registerAll = handlebars => {
|
||||
module.exports.registerMinimum(handlebars)
|
||||
// register imported helpers
|
||||
externalHandlebars.registerAll(handlebars)
|
||||
}
|
||||
|
||||
module.exports.unregisterAll = handlebars => {
|
||||
for (let helper of HELPERS) {
|
||||
helper.unregister(handlebars)
|
||||
}
|
||||
// unregister all imported helpers
|
||||
externalHandlebars.unregisterAll(handlebars)
|
||||
}
|
||||
|
||||
module.exports.getJsHelperList = getJsHelperList
|
|
@ -0,0 +1,103 @@
|
|||
import Helper from "./Helper"
|
||||
import { SafeString } from "handlebars"
|
||||
import * as externalHandlebars from "./external"
|
||||
import { processJS } from "./javascript"
|
||||
import {
|
||||
HelperFunctionNames,
|
||||
HelperFunctionBuiltin,
|
||||
LITERAL_MARKER,
|
||||
} from "./constants"
|
||||
|
||||
export { getJsHelperList } from "./list"
|
||||
|
||||
const HTML_SWAPS = {
|
||||
"<": "<",
|
||||
">": ">",
|
||||
}
|
||||
|
||||
function isObject(value: string | any[]) {
|
||||
if (value == null || typeof value !== "object") {
|
||||
return false
|
||||
}
|
||||
return (
|
||||
value.toString() === "[object Object]" ||
|
||||
(value.length > 0 && typeof value[0] === "object")
|
||||
)
|
||||
}
|
||||
|
||||
const HELPERS = [
|
||||
// external helpers
|
||||
new Helper(HelperFunctionNames.OBJECT, (value: any) => {
|
||||
return new SafeString(JSON.stringify(value))
|
||||
}),
|
||||
// javascript helper
|
||||
new Helper(HelperFunctionNames.JS, processJS, false),
|
||||
// this help is applied to all statements
|
||||
new Helper(
|
||||
HelperFunctionNames.ALL,
|
||||
(value: string, inputs: { __opts: any }) => {
|
||||
const { __opts } = inputs
|
||||
if (isObject(value)) {
|
||||
return new SafeString(JSON.stringify(value))
|
||||
}
|
||||
// null/undefined values produce bad results
|
||||
if (__opts && __opts.onlyFound && value == null) {
|
||||
return __opts.input
|
||||
}
|
||||
if (value == null || typeof value !== "string") {
|
||||
return value == null ? "" : value
|
||||
}
|
||||
// TODO: check, this should always be false
|
||||
if (value && (value as any).string) {
|
||||
value = (value as any).string
|
||||
}
|
||||
let text: any = value
|
||||
if (__opts && __opts.escapeNewlines) {
|
||||
text = value.replace(/\n/g, "\\n")
|
||||
}
|
||||
text = new SafeString(text.replace(/&/g, "&"))
|
||||
if (text == null || typeof text !== "string") {
|
||||
return text
|
||||
}
|
||||
return text.replace(/[<>]/g, (tag: string) => {
|
||||
return HTML_SWAPS[tag as keyof typeof HTML_SWAPS] || tag
|
||||
})
|
||||
}
|
||||
),
|
||||
// adds a note for post-processor
|
||||
new Helper(HelperFunctionNames.LITERAL, (value: any) => {
|
||||
if (value === undefined) {
|
||||
return ""
|
||||
}
|
||||
const type = typeof value
|
||||
const outputVal = type === "object" ? JSON.stringify(value) : value
|
||||
return `{{${LITERAL_MARKER} ${type}-${outputVal}}}`
|
||||
}),
|
||||
]
|
||||
|
||||
export function HelperNames() {
|
||||
return Object.values(HelperFunctionNames).concat(
|
||||
HelperFunctionBuiltin,
|
||||
externalHandlebars.externalHelperNames
|
||||
)
|
||||
}
|
||||
|
||||
export function registerMinimum(handlebars: typeof Handlebars) {
|
||||
for (let helper of HELPERS) {
|
||||
helper.register(handlebars)
|
||||
}
|
||||
}
|
||||
|
||||
export function registerAll(handlebars: typeof Handlebars) {
|
||||
registerMinimum(handlebars)
|
||||
// register imported helpers
|
||||
externalHandlebars.registerAll(handlebars)
|
||||
}
|
||||
|
||||
export function unregisterAll(handlebars: any) {
|
||||
for (let helper of HELPERS) {
|
||||
helper.unregister(handlebars)
|
||||
}
|
||||
// unregister all imported helpers
|
||||
externalHandlebars.unregisterAll(handlebars)
|
||||
}
|
|
@ -1,22 +1,24 @@
|
|||
const { atob, isBackendService, isJSAllowed } = require("../utilities")
|
||||
const cloneDeep = require("lodash.clonedeep")
|
||||
const { LITERAL_MARKER } = require("../helpers/constants")
|
||||
const { getJsHelperList } = require("./list")
|
||||
const { iifeWrapper } = require("../iife")
|
||||
import { atob, isJSAllowed } from "../utilities"
|
||||
import cloneDeep from "lodash/fp/cloneDeep"
|
||||
import { LITERAL_MARKER } from "../helpers/constants"
|
||||
import { getJsHelperList } from "./list"
|
||||
import { iifeWrapper } from "../iife"
|
||||
|
||||
// The method of executing JS scripts depends on the bundle being built.
|
||||
// This setter is used in the entrypoint (either index.js or index.mjs).
|
||||
let runJS
|
||||
module.exports.setJSRunner = runner => (runJS = runner)
|
||||
module.exports.removeJSRunner = () => {
|
||||
let runJS: ((js: string, context: any) => any) | undefined = undefined
|
||||
export const setJSRunner = (runner: typeof runJS) => (runJS = runner)
|
||||
|
||||
export const removeJSRunner = () => {
|
||||
runJS = undefined
|
||||
}
|
||||
|
||||
let onErrorLog
|
||||
module.exports.setOnErrorLog = delegate => (onErrorLog = delegate)
|
||||
let onErrorLog: (message: Error) => void
|
||||
export const setOnErrorLog = (delegate: typeof onErrorLog) =>
|
||||
(onErrorLog = delegate)
|
||||
|
||||
// Helper utility to strip square brackets from a value
|
||||
const removeSquareBrackets = value => {
|
||||
const removeSquareBrackets = (value: string) => {
|
||||
if (!value || typeof value !== "string") {
|
||||
return value
|
||||
}
|
||||
|
@ -30,7 +32,7 @@ const removeSquareBrackets = value => {
|
|||
|
||||
// Our context getter function provided to JS code as $.
|
||||
// Extracts a value from context.
|
||||
const getContextValue = (path, context) => {
|
||||
const getContextValue = (path: string, context: any) => {
|
||||
let data = context
|
||||
path.split(".").forEach(key => {
|
||||
if (data == null || typeof data !== "object") {
|
||||
|
@ -42,8 +44,8 @@ const getContextValue = (path, context) => {
|
|||
}
|
||||
|
||||
// Evaluates JS code against a certain context
|
||||
module.exports.processJS = (handlebars, context) => {
|
||||
if (!isJSAllowed() || (isBackendService() && !runJS)) {
|
||||
export function processJS(handlebars: string, context: any) {
|
||||
if (!isJSAllowed() || !runJS) {
|
||||
throw new Error("JS disabled in environment.")
|
||||
}
|
||||
try {
|
||||
|
@ -53,8 +55,8 @@ module.exports.processJS = (handlebars, context) => {
|
|||
|
||||
// Transform snippets into an object for faster access, and cache previously
|
||||
// evaluated snippets
|
||||
let snippetMap = {}
|
||||
let snippetCache = {}
|
||||
let snippetMap: any = {}
|
||||
let snippetCache: any = {}
|
||||
for (let snippet of context.snippets || []) {
|
||||
snippetMap[snippet.name] = snippet.code
|
||||
}
|
||||
|
@ -64,7 +66,7 @@ module.exports.processJS = (handlebars, context) => {
|
|||
// app context.
|
||||
const clonedContext = cloneDeep({ ...context, snippets: null })
|
||||
const sandboxContext = {
|
||||
$: path => getContextValue(path, clonedContext),
|
||||
$: (path: string) => getContextValue(path, clonedContext),
|
||||
helpers: getJsHelperList(),
|
||||
|
||||
// Proxy to evaluate snippets when running in the browser
|
||||
|
@ -84,7 +86,7 @@ module.exports.processJS = (handlebars, context) => {
|
|||
// Create a sandbox with our context and run the JS
|
||||
const res = { data: runJS(js, sandboxContext) }
|
||||
return `{{${LITERAL_MARKER} js_result-${JSON.stringify(res)}}}`
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
onErrorLog && onErrorLog(error)
|
||||
|
||||
if (error.code === "ERR_SCRIPT_EXECUTION_TIMEOUT") {
|
|
@ -1,7 +1,7 @@
|
|||
const { date, duration } = require("./date")
|
||||
import { date, duration } from "./date"
|
||||
|
||||
// https://github.com/evanw/esbuild/issues/56
|
||||
const externalCollections = {
|
||||
const getExternalCollections = (): Record<string, () => any> => ({
|
||||
math: require("@budibase/handlebars-helpers/lib/math"),
|
||||
array: require("@budibase/handlebars-helpers/lib/array"),
|
||||
number: require("@budibase/handlebars-helpers/lib/number"),
|
||||
|
@ -11,32 +11,32 @@ const externalCollections = {
|
|||
object: require("@budibase/handlebars-helpers/lib/object"),
|
||||
regex: require("@budibase/handlebars-helpers/lib/regex"),
|
||||
uuid: require("@budibase/handlebars-helpers/lib/uuid"),
|
||||
}
|
||||
})
|
||||
|
||||
const helpersToRemoveForJs = ["sortBy"]
|
||||
module.exports.helpersToRemoveForJs = helpersToRemoveForJs
|
||||
export const helpersToRemoveForJs = ["sortBy"]
|
||||
|
||||
const addedHelpers = {
|
||||
date: date,
|
||||
duration: duration,
|
||||
}
|
||||
|
||||
let helpers = undefined
|
||||
let helpers: Record<string, any>
|
||||
|
||||
module.exports.getJsHelperList = () => {
|
||||
export function getJsHelperList() {
|
||||
if (helpers) {
|
||||
return helpers
|
||||
}
|
||||
|
||||
helpers = {}
|
||||
for (let collection of Object.values(externalCollections)) {
|
||||
for (let collection of Object.values(getExternalCollections())) {
|
||||
for (let [key, func] of Object.entries(collection)) {
|
||||
// Handlebars injects the hbs options to the helpers by default. We are adding an empty {} as a last parameter to simulate it
|
||||
helpers[key] = (...props) => func(...props, {})
|
||||
helpers[key] = (...props: any) => func(...props, {})
|
||||
}
|
||||
}
|
||||
for (let key of Object.keys(addedHelpers)) {
|
||||
helpers[key] = addedHelpers[key]
|
||||
helpers = {
|
||||
...helpers,
|
||||
addedHelpers,
|
||||
}
|
||||
|
||||
for (const toRemove of helpersToRemoveForJs) {
|
|
@ -1,3 +0,0 @@
|
|||
module.exports.iifeWrapper = script => {
|
||||
return `(function(){\n${script}\n})();`
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export const iifeWrapper = (script: string) => {
|
||||
return `(function(){\n${script}\n})();`
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
import templates from "./index.js"
|
||||
|
||||
/**
|
||||
* ES6 entrypoint for rollup
|
||||
*/
|
||||
export const isValid = templates.isValid
|
||||
export const makePropSafe = templates.makePropSafe
|
||||
export const getManifest = templates.getManifest
|
||||
export const isJSBinding = templates.isJSBinding
|
||||
export const encodeJSBinding = templates.encodeJSBinding
|
||||
export const decodeJSBinding = templates.decodeJSBinding
|
||||
export const processStringSync = templates.processStringSync
|
||||
export const processObjectSync = templates.processObjectSync
|
||||
export const processString = templates.processString
|
||||
export const processObject = templates.processObject
|
||||
export const doesContainStrings = templates.doesContainStrings
|
||||
export const doesContainString = templates.doesContainString
|
||||
export const disableEscaping = templates.disableEscaping
|
||||
export const findHBSBlocks = templates.findHBSBlocks
|
||||
export const convertToJS = templates.convertToJS
|
||||
export const setJSRunner = templates.setJSRunner
|
||||
export const setOnErrorLog = templates.setOnErrorLog
|
||||
export const FIND_ANY_HBS_REGEX = templates.FIND_ANY_HBS_REGEX
|
||||
export const helpersToRemoveForJs = templates.helpersToRemoveForJs
|
||||
|
||||
export * from "./errors.js"
|
|
@ -1,24 +1,30 @@
|
|||
const vm = require("vm")
|
||||
const handlebars = require("handlebars")
|
||||
const { registerAll, registerMinimum } = require("./helpers/index")
|
||||
const processors = require("./processors")
|
||||
const { atob, btoa, isBackendService } = require("./utilities")
|
||||
const { iifeWrapper } = require("./iife")
|
||||
const manifest = require("../manifest.json")
|
||||
const {
|
||||
import { Context, createContext, runInNewContext } from "vm"
|
||||
import { create } from "handlebars"
|
||||
import { registerAll, registerMinimum } from "./helpers/index"
|
||||
import { preprocess, postprocess } from "./processors"
|
||||
import {
|
||||
atob,
|
||||
btoa,
|
||||
isBackendService,
|
||||
FIND_HBS_REGEX,
|
||||
FIND_ANY_HBS_REGEX,
|
||||
findDoubleHbsInstances,
|
||||
} = require("./utilities")
|
||||
const { convertHBSBlock } = require("./conversion")
|
||||
const javascript = require("./helpers/javascript")
|
||||
const { helpersToRemoveForJs } = require("./helpers/list")
|
||||
} from "./utilities"
|
||||
import { convertHBSBlock } from "./conversion"
|
||||
import { setJSRunner, removeJSRunner } from "./helpers/javascript"
|
||||
import { helpersToRemoveForJs } from "./helpers/list"
|
||||
|
||||
const hbsInstance = handlebars.create()
|
||||
import manifest from "./manifest.json"
|
||||
import { ProcessOptions } from "./types"
|
||||
|
||||
export { setJSRunner, setOnErrorLog } from "./helpers/javascript"
|
||||
export { iifeWrapper } from "./iife"
|
||||
|
||||
const hbsInstance = create()
|
||||
registerAll(hbsInstance)
|
||||
const hbsInstanceNoHelpers = handlebars.create()
|
||||
const hbsInstanceNoHelpers = create()
|
||||
registerMinimum(hbsInstanceNoHelpers)
|
||||
const defaultOpts = {
|
||||
const defaultOpts: ProcessOptions = {
|
||||
noHelpers: false,
|
||||
cacheTemplates: false,
|
||||
noEscaping: false,
|
||||
|
@ -29,7 +35,7 @@ const defaultOpts = {
|
|||
/**
|
||||
* Utility function to check if the object is valid.
|
||||
*/
|
||||
function testObject(object) {
|
||||
function testObject(object: any) {
|
||||
// JSON stringify will fail if there are any cycles, stops infinite recursion
|
||||
try {
|
||||
JSON.stringify(object)
|
||||
|
@ -41,8 +47,8 @@ function testObject(object) {
|
|||
/**
|
||||
* Creates a HBS template function for a given string, and optionally caches it.
|
||||
*/
|
||||
let templateCache = {}
|
||||
function createTemplate(string, opts) {
|
||||
const templateCache: Record<string, HandlebarsTemplateDelegate<any>> = {}
|
||||
function createTemplate(string: string, opts?: ProcessOptions) {
|
||||
opts = { ...defaultOpts, ...opts }
|
||||
|
||||
// Finalising adds a helper, can't do this with no helpers
|
||||
|
@ -53,11 +59,11 @@ function createTemplate(string, opts) {
|
|||
return templateCache[key]
|
||||
}
|
||||
|
||||
string = processors.preprocess(string, opts)
|
||||
string = preprocess(string, opts)
|
||||
|
||||
// Optionally disable built in HBS escaping
|
||||
if (opts.noEscaping) {
|
||||
string = exports.disableEscaping(string)
|
||||
string = disableEscaping(string)
|
||||
}
|
||||
|
||||
// This does not throw an error when template can't be fulfilled,
|
||||
|
@ -78,24 +84,25 @@ function createTemplate(string, opts) {
|
|||
* @param {object|undefined} [opts] optional - specify some options for processing.
|
||||
* @returns {Promise<object|array>} The structure input, as fully updated as possible.
|
||||
*/
|
||||
module.exports.processObject = async (object, context, opts) => {
|
||||
export async function processObject<T extends Record<string, any>>(
|
||||
object: T,
|
||||
context: object,
|
||||
opts?: { noHelpers?: boolean; escapeNewlines?: boolean; onlyFound?: boolean }
|
||||
): Promise<T> {
|
||||
testObject(object)
|
||||
for (let key of Object.keys(object || {})) {
|
||||
|
||||
for (const key of Object.keys(object || {})) {
|
||||
if (object[key] != null) {
|
||||
let val = object[key]
|
||||
const val = object[key]
|
||||
let parsedValue
|
||||
if (typeof val === "string") {
|
||||
object[key] = await module.exports.processString(
|
||||
object[key],
|
||||
context,
|
||||
opts
|
||||
)
|
||||
parsedValue = await processString(object[key], context, opts)
|
||||
} else if (typeof val === "object") {
|
||||
object[key] = await module.exports.processObject(
|
||||
object[key],
|
||||
context,
|
||||
opts
|
||||
)
|
||||
parsedValue = await processObject(object[key], context, opts)
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
object[key] = parsedValue
|
||||
}
|
||||
}
|
||||
return object
|
||||
|
@ -109,9 +116,13 @@ module.exports.processObject = async (object, context, opts) => {
|
|||
* @param {object|undefined} [opts] optional - specify some options for processing.
|
||||
* @returns {Promise<string>} The enriched string, all templates should have been replaced if they can be.
|
||||
*/
|
||||
module.exports.processString = async (string, context, opts) => {
|
||||
export async function processString(
|
||||
string: string,
|
||||
context: object,
|
||||
opts?: ProcessOptions
|
||||
): Promise<string> {
|
||||
// TODO: carry out any async calls before carrying out async call
|
||||
return module.exports.processStringSync(string, context, opts)
|
||||
return processStringSync(string, context, opts)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -123,14 +134,18 @@ module.exports.processString = async (string, context, opts) => {
|
|||
* @param {object|undefined} [opts] optional - specify some options for processing.
|
||||
* @returns {object|array} The structure input, as fully updated as possible.
|
||||
*/
|
||||
module.exports.processObjectSync = (object, context, opts) => {
|
||||
export function processObjectSync(
|
||||
object: { [x: string]: any },
|
||||
context: any,
|
||||
opts: any
|
||||
): object | Array<any> {
|
||||
testObject(object)
|
||||
for (let key of Object.keys(object || {})) {
|
||||
let val = object[key]
|
||||
if (typeof val === "string") {
|
||||
object[key] = module.exports.processStringSync(object[key], context, opts)
|
||||
object[key] = processStringSync(object[key], context, opts)
|
||||
} else if (typeof val === "object") {
|
||||
object[key] = module.exports.processObjectSync(object[key], context, opts)
|
||||
object[key] = processObjectSync(object[key], context, opts)
|
||||
}
|
||||
}
|
||||
return object
|
||||
|
@ -144,29 +159,32 @@ module.exports.processObjectSync = (object, context, opts) => {
|
|||
* @param {object|undefined} [opts] optional - specify some options for processing.
|
||||
* @returns {string} The enriched string, all templates should have been replaced if they can be.
|
||||
*/
|
||||
module.exports.processStringSync = (string, context, opts) => {
|
||||
export function processStringSync(
|
||||
string: string,
|
||||
context?: object,
|
||||
opts?: ProcessOptions
|
||||
): string {
|
||||
// Take a copy of input in case of error
|
||||
const input = string
|
||||
if (typeof string !== "string") {
|
||||
throw "Cannot process non-string types."
|
||||
}
|
||||
function process(stringPart) {
|
||||
function process(stringPart: string) {
|
||||
const template = createTemplate(stringPart, opts)
|
||||
const now = Math.floor(Date.now() / 1000) * 1000
|
||||
return processors.postprocess(
|
||||
template({
|
||||
now: new Date(now).toISOString(),
|
||||
__opts: {
|
||||
...opts,
|
||||
input: stringPart,
|
||||
},
|
||||
...context,
|
||||
})
|
||||
)
|
||||
const processedString = template({
|
||||
now: new Date(now).toISOString(),
|
||||
__opts: {
|
||||
...opts,
|
||||
input: stringPart,
|
||||
},
|
||||
...context,
|
||||
})
|
||||
return postprocess(processedString)
|
||||
}
|
||||
try {
|
||||
if (opts && opts.onlyFound) {
|
||||
const blocks = exports.findHBSBlocks(string)
|
||||
const blocks = findHBSBlocks(string)
|
||||
for (let block of blocks) {
|
||||
const outcome = process(block)
|
||||
string = string.replace(block, outcome)
|
||||
|
@ -186,7 +204,7 @@ module.exports.processStringSync = (string, context, opts) => {
|
|||
* this function will find any double braces and switch to triple.
|
||||
* @param string the string to have double HBS statements converted to triple.
|
||||
*/
|
||||
module.exports.disableEscaping = string => {
|
||||
export function disableEscaping(string: string) {
|
||||
const matches = findDoubleHbsInstances(string)
|
||||
if (matches == null) {
|
||||
return string
|
||||
|
@ -207,7 +225,7 @@ module.exports.disableEscaping = string => {
|
|||
* @param {string} property The property which is to be wrapped.
|
||||
* @returns {string} The wrapped property ready to be added to a templating string.
|
||||
*/
|
||||
module.exports.makePropSafe = property => {
|
||||
export function makePropSafe(property: any): string {
|
||||
return `[${property}]`.replace("[[", "[").replace("]]", "]")
|
||||
}
|
||||
|
||||
|
@ -217,7 +235,7 @@ module.exports.makePropSafe = property => {
|
|||
* @param [opts] optional - specify some options for processing.
|
||||
* @returns {boolean} Whether or not the input string is valid.
|
||||
*/
|
||||
module.exports.isValid = (string, opts) => {
|
||||
export function isValid(string: any, opts?: any): boolean {
|
||||
const validCases = [
|
||||
"string",
|
||||
"number",
|
||||
|
@ -238,7 +256,7 @@ module.exports.isValid = (string, opts) => {
|
|||
})
|
||||
template(context)
|
||||
return true
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
const msg = err && err.message ? err.message : err
|
||||
if (!msg) {
|
||||
return false
|
||||
|
@ -259,7 +277,7 @@ module.exports.isValid = (string, opts) => {
|
|||
* This manifest provides information about each of the helpers and how it can be used.
|
||||
* @returns The manifest JSON which has been generated from the helpers.
|
||||
*/
|
||||
module.exports.getManifest = () => {
|
||||
export function getManifest() {
|
||||
return manifest
|
||||
}
|
||||
|
||||
|
@ -268,8 +286,8 @@ module.exports.getManifest = () => {
|
|||
* @param handlebars the HBS expression to check
|
||||
* @returns {boolean} whether the expression is JS or not
|
||||
*/
|
||||
module.exports.isJSBinding = handlebars => {
|
||||
return module.exports.decodeJSBinding(handlebars) != null
|
||||
export function isJSBinding(handlebars: any): boolean {
|
||||
return decodeJSBinding(handlebars) != null
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -277,7 +295,7 @@ module.exports.isJSBinding = handlebars => {
|
|||
* @param javascript the JS code to encode
|
||||
* @returns {string} the JS HBS expression
|
||||
*/
|
||||
module.exports.encodeJSBinding = javascript => {
|
||||
export function encodeJSBinding(javascript: string): string {
|
||||
return `{{ js "${btoa(javascript)}" }}`
|
||||
}
|
||||
|
||||
|
@ -286,7 +304,7 @@ module.exports.encodeJSBinding = javascript => {
|
|||
* @param handlebars the JS HBS expression
|
||||
* @returns {string|null} the raw JS code
|
||||
*/
|
||||
module.exports.decodeJSBinding = handlebars => {
|
||||
export function decodeJSBinding(handlebars: string): string | null {
|
||||
if (!handlebars || typeof handlebars !== "string") {
|
||||
return null
|
||||
}
|
||||
|
@ -311,7 +329,7 @@ module.exports.decodeJSBinding = handlebars => {
|
|||
* @param {string[]} strings The strings to look for.
|
||||
* @returns {boolean} Will return true if all strings found in HBS statement.
|
||||
*/
|
||||
module.exports.doesContainStrings = (template, strings) => {
|
||||
export function doesContainStrings(template: string, strings: any[]): boolean {
|
||||
let regexp = new RegExp(FIND_HBS_REGEX)
|
||||
let matches = template.match(regexp)
|
||||
if (matches == null) {
|
||||
|
@ -319,8 +337,8 @@ module.exports.doesContainStrings = (template, strings) => {
|
|||
}
|
||||
for (let match of matches) {
|
||||
let hbs = match
|
||||
if (exports.isJSBinding(match)) {
|
||||
hbs = exports.decodeJSBinding(match)
|
||||
if (isJSBinding(match)) {
|
||||
hbs = decodeJSBinding(match)!
|
||||
}
|
||||
let allFound = true
|
||||
for (let string of strings) {
|
||||
|
@ -341,7 +359,7 @@ module.exports.doesContainStrings = (template, strings) => {
|
|||
* @param {string} string The string to search within.
|
||||
* @return {string[]} The found HBS blocks.
|
||||
*/
|
||||
module.exports.findHBSBlocks = string => {
|
||||
export function findHBSBlocks(string: string): string[] {
|
||||
if (!string || typeof string !== "string") {
|
||||
return []
|
||||
}
|
||||
|
@ -362,18 +380,15 @@ module.exports.findHBSBlocks = string => {
|
|||
* @param {string} string The word or sentence to search for.
|
||||
* @returns {boolean} The this return true if the string is found, false if not.
|
||||
*/
|
||||
module.exports.doesContainString = (template, string) => {
|
||||
return exports.doesContainStrings(template, [string])
|
||||
export function doesContainString(template: any, string: any): boolean {
|
||||
return doesContainStrings(template, [string])
|
||||
}
|
||||
|
||||
module.exports.setJSRunner = javascript.setJSRunner
|
||||
module.exports.setOnErrorLog = javascript.setOnErrorLog
|
||||
|
||||
module.exports.convertToJS = hbs => {
|
||||
const blocks = exports.findHBSBlocks(hbs)
|
||||
export function convertToJS(hbs: string) {
|
||||
const blocks = findHBSBlocks(hbs)
|
||||
let js = "return `",
|
||||
prevBlock = null
|
||||
const variables = {}
|
||||
prevBlock: string | null = null
|
||||
const variables: Record<string, any> = {}
|
||||
if (blocks.length === 0) {
|
||||
js += hbs
|
||||
}
|
||||
|
@ -387,7 +402,7 @@ module.exports.convertToJS = hbs => {
|
|||
prevBlock = block
|
||||
const { variable, value } = convertHBSBlock(block, count++)
|
||||
variables[variable] = value
|
||||
js += `${stringPart.split()}\${${variable}}`
|
||||
js += `${[stringPart]}\${${variable}}`
|
||||
}
|
||||
let varBlock = ""
|
||||
for (let [variable, value] of Object.entries(variables)) {
|
||||
|
@ -397,34 +412,34 @@ module.exports.convertToJS = hbs => {
|
|||
return `${varBlock}${js}`
|
||||
}
|
||||
|
||||
module.exports.FIND_ANY_HBS_REGEX = FIND_ANY_HBS_REGEX
|
||||
const _FIND_ANY_HBS_REGEX = FIND_ANY_HBS_REGEX
|
||||
export { _FIND_ANY_HBS_REGEX as FIND_ANY_HBS_REGEX }
|
||||
|
||||
const errors = require("./errors")
|
||||
// We cannot use dynamic exports, otherwise the typescript file will not be generating it
|
||||
module.exports.JsErrorTimeout = errors.JsErrorTimeout
|
||||
export { JsErrorTimeout } from "./errors"
|
||||
|
||||
module.exports.helpersToRemoveForJs = helpersToRemoveForJs
|
||||
const _helpersToRemoveForJs = helpersToRemoveForJs
|
||||
export { _helpersToRemoveForJs as helpersToRemoveForJs }
|
||||
|
||||
function defaultJSSetup() {
|
||||
if (!isBackendService()) {
|
||||
/**
|
||||
* Use polyfilled vm to run JS scripts in a browser Env
|
||||
*/
|
||||
javascript.setJSRunner((js, context) => {
|
||||
setJSRunner((js: string, context: Context) => {
|
||||
context = {
|
||||
...context,
|
||||
alert: undefined,
|
||||
setInterval: undefined,
|
||||
setTimeout: undefined,
|
||||
}
|
||||
vm.createContext(context)
|
||||
return vm.runInNewContext(js, context, { timeout: 1000 })
|
||||
createContext(context)
|
||||
return runInNewContext(js, context, { timeout: 1000 })
|
||||
})
|
||||
} else {
|
||||
javascript.removeJSRunner()
|
||||
removeJSRunner()
|
||||
}
|
||||
}
|
||||
defaultJSSetup()
|
||||
|
||||
module.exports.defaultJSSetup = defaultJSSetup
|
||||
module.exports.iifeWrapper = iifeWrapper
|
||||
const _defaultJSSetup = defaultJSSetup
|
||||
export { _defaultJSSetup as defaultJSSetup }
|
|
@ -1,8 +1,9 @@
|
|||
const { FIND_HBS_REGEX } = require("../utilities")
|
||||
const preprocessor = require("./preprocessor")
|
||||
const postprocessor = require("./postprocessor")
|
||||
import { FIND_HBS_REGEX } from "../utilities"
|
||||
import * as preprocessor from "./preprocessor"
|
||||
import * as postprocessor from "./postprocessor"
|
||||
import { ProcessOptions } from "../types"
|
||||
|
||||
function process(output, processors, opts) {
|
||||
function process(output: string, processors: any[], opts?: ProcessOptions) {
|
||||
for (let processor of processors) {
|
||||
// if a literal statement has occurred stop
|
||||
if (typeof output !== "string") {
|
||||
|
@ -21,7 +22,7 @@ function process(output, processors, opts) {
|
|||
return output
|
||||
}
|
||||
|
||||
module.exports.preprocess = (string, opts) => {
|
||||
export function preprocess(string: string, opts: ProcessOptions) {
|
||||
let processors = preprocessor.processors
|
||||
if (opts.noFinalise) {
|
||||
processors = processors.filter(
|
||||
|
@ -30,7 +31,7 @@ module.exports.preprocess = (string, opts) => {
|
|||
}
|
||||
return process(string, processors, opts)
|
||||
}
|
||||
module.exports.postprocess = string => {
|
||||
export function postprocess(string: string) {
|
||||
let processors = postprocessor.processors
|
||||
return process(string, processors)
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
const { LITERAL_MARKER } = require("../helpers/constants")
|
||||
|
||||
const PostProcessorNames = {
|
||||
CONVERT_LITERALS: "convert-literals",
|
||||
}
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
class Postprocessor {
|
||||
constructor(name, fn) {
|
||||
this.name = name
|
||||
this.fn = fn
|
||||
}
|
||||
|
||||
process(statement) {
|
||||
return this.fn(statement)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.PostProcessorNames = PostProcessorNames
|
||||
|
||||
module.exports.processors = [
|
||||
new Postprocessor(PostProcessorNames.CONVERT_LITERALS, statement => {
|
||||
if (typeof statement !== "string" || !statement.includes(LITERAL_MARKER)) {
|
||||
return statement
|
||||
}
|
||||
const splitMarkerIndex = statement.indexOf("-")
|
||||
const type = statement.substring(12, splitMarkerIndex)
|
||||
const value = statement.substring(
|
||||
splitMarkerIndex + 1,
|
||||
statement.length - 2
|
||||
)
|
||||
switch (type) {
|
||||
case "string":
|
||||
return value
|
||||
case "number":
|
||||
return parseFloat(value)
|
||||
case "boolean":
|
||||
return value === "true"
|
||||
case "object":
|
||||
return JSON.parse(value)
|
||||
case "js_result":
|
||||
// We use the literal helper to process the result of JS expressions
|
||||
// as we want to be able to return any types.
|
||||
// We wrap the value in an abject to be able to use undefined properly.
|
||||
return JSON.parse(value).data
|
||||
}
|
||||
return value
|
||||
}),
|
||||
]
|
|
@ -0,0 +1,56 @@
|
|||
import { LITERAL_MARKER } from "../helpers/constants"
|
||||
|
||||
export const PostProcessorNames = {
|
||||
CONVERT_LITERALS: "convert-literals",
|
||||
}
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
class Postprocessor {
|
||||
name: string
|
||||
private fn: any
|
||||
|
||||
constructor(name: string, fn: any) {
|
||||
this.name = name
|
||||
this.fn = fn
|
||||
}
|
||||
|
||||
process(statement: any) {
|
||||
return this.fn(statement)
|
||||
}
|
||||
}
|
||||
|
||||
export const processors = [
|
||||
new Postprocessor(
|
||||
PostProcessorNames.CONVERT_LITERALS,
|
||||
(statement: string) => {
|
||||
if (
|
||||
typeof statement !== "string" ||
|
||||
!statement.includes(LITERAL_MARKER)
|
||||
) {
|
||||
return statement
|
||||
}
|
||||
const splitMarkerIndex = statement.indexOf("-")
|
||||
const type = statement.substring(12, splitMarkerIndex)
|
||||
const value = statement.substring(
|
||||
splitMarkerIndex + 1,
|
||||
statement.length - 2
|
||||
)
|
||||
switch (type) {
|
||||
case "string":
|
||||
return value
|
||||
case "number":
|
||||
return parseFloat(value)
|
||||
case "boolean":
|
||||
return value === "true"
|
||||
case "object":
|
||||
return JSON.parse(value)
|
||||
case "js_result":
|
||||
// We use the literal helper to process the result of JS expressions
|
||||
// as we want to be able to return any types.
|
||||
// We wrap the value in an abject to be able to use undefined properly.
|
||||
return JSON.parse(value).data
|
||||
}
|
||||
return value
|
||||
}
|
||||
),
|
||||
]
|
|
@ -1,78 +0,0 @@
|
|||
const { HelperNames } = require("../helpers")
|
||||
const { swapStrings, isAlphaNumeric } = require("../utilities")
|
||||
|
||||
const FUNCTION_CASES = ["#", "else", "/"]
|
||||
|
||||
const PreprocessorNames = {
|
||||
SWAP_TO_DOT: "swap-to-dot-notation",
|
||||
FIX_FUNCTIONS: "fix-functions",
|
||||
FINALISE: "finalise",
|
||||
}
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
class Preprocessor {
|
||||
constructor(name, fn) {
|
||||
this.name = name
|
||||
this.fn = fn
|
||||
}
|
||||
|
||||
process(fullString, statement, opts) {
|
||||
const output = this.fn(statement, opts)
|
||||
const idx = fullString.indexOf(statement)
|
||||
return swapStrings(fullString, idx, statement.length, output)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.processors = [
|
||||
new Preprocessor(PreprocessorNames.SWAP_TO_DOT, statement => {
|
||||
let startBraceIdx = statement.indexOf("[")
|
||||
let lastIdx = 0
|
||||
while (startBraceIdx !== -1) {
|
||||
// if the character previous to the literal specifier is alphanumeric this should happen
|
||||
if (isAlphaNumeric(statement.charAt(startBraceIdx - 1))) {
|
||||
statement = swapStrings(statement, startBraceIdx + lastIdx, 1, ".[")
|
||||
}
|
||||
lastIdx = startBraceIdx + 1
|
||||
const nextBraceIdx = statement.substring(lastIdx + 1).indexOf("[")
|
||||
startBraceIdx = nextBraceIdx > 0 ? lastIdx + 1 + nextBraceIdx : -1
|
||||
}
|
||||
return statement
|
||||
}),
|
||||
|
||||
new Preprocessor(PreprocessorNames.FIX_FUNCTIONS, statement => {
|
||||
for (let specialCase of FUNCTION_CASES) {
|
||||
const toFind = `{ ${specialCase}`,
|
||||
replacement = `{${specialCase}`
|
||||
statement = statement.replace(new RegExp(toFind, "g"), replacement)
|
||||
}
|
||||
return statement
|
||||
}),
|
||||
|
||||
new Preprocessor(PreprocessorNames.FINALISE, (statement, opts) => {
|
||||
const noHelpers = opts && opts.noHelpers
|
||||
let insideStatement = statement.slice(2, statement.length - 2)
|
||||
if (insideStatement.charAt(0) === " ") {
|
||||
insideStatement = insideStatement.slice(1)
|
||||
}
|
||||
if (insideStatement.charAt(insideStatement.length - 1) === " ") {
|
||||
insideStatement = insideStatement.slice(0, insideStatement.length - 1)
|
||||
}
|
||||
const possibleHelper = insideStatement.split(" ")[0]
|
||||
// function helpers can't be wrapped
|
||||
for (let specialCase of FUNCTION_CASES) {
|
||||
if (possibleHelper.includes(specialCase)) {
|
||||
return statement
|
||||
}
|
||||
}
|
||||
const testHelper = possibleHelper.trim().toLowerCase()
|
||||
if (
|
||||
!noHelpers &&
|
||||
HelperNames().some(option => testHelper === option.toLowerCase())
|
||||
) {
|
||||
insideStatement = `(${insideStatement})`
|
||||
}
|
||||
return `{{ all ${insideStatement} }}`
|
||||
}),
|
||||
]
|
||||
|
||||
module.exports.PreprocessorNames = PreprocessorNames
|
|
@ -0,0 +1,82 @@
|
|||
import { HelperNames } from "../helpers"
|
||||
import { swapStrings, isAlphaNumeric } from "../utilities"
|
||||
|
||||
const FUNCTION_CASES = ["#", "else", "/"]
|
||||
|
||||
export const PreprocessorNames = {
|
||||
SWAP_TO_DOT: "swap-to-dot-notation",
|
||||
FIX_FUNCTIONS: "fix-functions",
|
||||
FINALISE: "finalise",
|
||||
}
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
class Preprocessor {
|
||||
name: string
|
||||
private fn: any
|
||||
|
||||
constructor(name: string, fn: any) {
|
||||
this.name = name
|
||||
this.fn = fn
|
||||
}
|
||||
|
||||
process(fullString: string, statement: string, opts: Object) {
|
||||
const output = this.fn(statement, opts)
|
||||
const idx = fullString.indexOf(statement)
|
||||
return swapStrings(fullString, idx, statement.length, output)
|
||||
}
|
||||
}
|
||||
|
||||
export const processors = [
|
||||
new Preprocessor(PreprocessorNames.SWAP_TO_DOT, (statement: string) => {
|
||||
let startBraceIdx = statement.indexOf("[")
|
||||
let lastIdx = 0
|
||||
while (startBraceIdx !== -1) {
|
||||
// if the character previous to the literal specifier is alphanumeric this should happen
|
||||
if (isAlphaNumeric(statement.charAt(startBraceIdx - 1))) {
|
||||
statement = swapStrings(statement, startBraceIdx + lastIdx, 1, ".[")
|
||||
}
|
||||
lastIdx = startBraceIdx + 1
|
||||
const nextBraceIdx = statement.substring(lastIdx + 1).indexOf("[")
|
||||
startBraceIdx = nextBraceIdx > 0 ? lastIdx + 1 + nextBraceIdx : -1
|
||||
}
|
||||
return statement
|
||||
}),
|
||||
|
||||
new Preprocessor(PreprocessorNames.FIX_FUNCTIONS, (statement: string) => {
|
||||
for (let specialCase of FUNCTION_CASES) {
|
||||
const toFind = `{ ${specialCase}`,
|
||||
replacement = `{${specialCase}`
|
||||
statement = statement.replace(new RegExp(toFind, "g"), replacement)
|
||||
}
|
||||
return statement
|
||||
}),
|
||||
|
||||
new Preprocessor(
|
||||
PreprocessorNames.FINALISE,
|
||||
(statement: string, opts: { noHelpers: any }) => {
|
||||
const noHelpers = opts && opts.noHelpers
|
||||
let insideStatement = statement.slice(2, statement.length - 2)
|
||||
if (insideStatement.charAt(0) === " ") {
|
||||
insideStatement = insideStatement.slice(1)
|
||||
}
|
||||
if (insideStatement.charAt(insideStatement.length - 1) === " ") {
|
||||
insideStatement = insideStatement.slice(0, insideStatement.length - 1)
|
||||
}
|
||||
const possibleHelper = insideStatement.split(" ")[0]
|
||||
// function helpers can't be wrapped
|
||||
for (let specialCase of FUNCTION_CASES) {
|
||||
if (possibleHelper.includes(specialCase)) {
|
||||
return statement
|
||||
}
|
||||
}
|
||||
const testHelper = possibleHelper.trim().toLowerCase()
|
||||
if (
|
||||
!noHelpers &&
|
||||
HelperNames().some(option => testHelper === option.toLowerCase())
|
||||
) {
|
||||
insideStatement = `(${insideStatement})`
|
||||
}
|
||||
return `{{ all ${insideStatement} }}`
|
||||
}
|
||||
),
|
||||
]
|
|
@ -0,0 +1,8 @@
|
|||
export interface ProcessOptions {
|
||||
cacheTemplates?: boolean
|
||||
noEscaping?: boolean
|
||||
noHelpers?: boolean
|
||||
noFinalise?: boolean
|
||||
escapeNewlines?: boolean
|
||||
onlyFound?: boolean
|
||||
}
|
|
@ -1,28 +1,28 @@
|
|||
const ALPHA_NUMERIC_REGEX = /^[A-Za-z0-9]+$/g
|
||||
|
||||
module.exports.FIND_HBS_REGEX = /{{([^{].*?)}}/g
|
||||
module.exports.FIND_ANY_HBS_REGEX = /{?{{([^{].*?)}}}?/g
|
||||
module.exports.FIND_TRIPLE_HBS_REGEX = /{{{([^{].*?)}}}/g
|
||||
export const FIND_HBS_REGEX = /{{([^{].*?)}}/g
|
||||
export const FIND_ANY_HBS_REGEX = /{?{{([^{].*?)}}}?/g
|
||||
export const FIND_TRIPLE_HBS_REGEX = /{{{([^{].*?)}}}/g
|
||||
|
||||
module.exports.isBackendService = () => {
|
||||
export const isBackendService = () => {
|
||||
return typeof window === "undefined"
|
||||
}
|
||||
|
||||
module.exports.isJSAllowed = () => {
|
||||
export const isJSAllowed = () => {
|
||||
return process && !process.env.NO_JS
|
||||
}
|
||||
|
||||
// originally this could be done with a single regex using look behinds
|
||||
// but safari does not support this feature
|
||||
// original regex: /(?<!{){{[^{}]+}}(?!})/g
|
||||
module.exports.findDoubleHbsInstances = string => {
|
||||
export const findDoubleHbsInstances = (string: string): string[] => {
|
||||
let copied = string
|
||||
const doubleRegex = new RegExp(exports.FIND_HBS_REGEX)
|
||||
const regex = new RegExp(exports.FIND_TRIPLE_HBS_REGEX)
|
||||
const doubleRegex = new RegExp(FIND_HBS_REGEX)
|
||||
const regex = new RegExp(FIND_TRIPLE_HBS_REGEX)
|
||||
const tripleMatches = copied.match(regex)
|
||||
// remove triple braces
|
||||
if (tripleMatches) {
|
||||
tripleMatches.forEach(match => {
|
||||
tripleMatches.forEach((match: string) => {
|
||||
copied = copied.replace(match, "")
|
||||
})
|
||||
}
|
||||
|
@ -30,34 +30,39 @@ module.exports.findDoubleHbsInstances = string => {
|
|||
return doubleMatches ? doubleMatches : []
|
||||
}
|
||||
|
||||
module.exports.isAlphaNumeric = char => {
|
||||
export const isAlphaNumeric = (char: string) => {
|
||||
return char.match(ALPHA_NUMERIC_REGEX)
|
||||
}
|
||||
|
||||
module.exports.swapStrings = (string, start, length, swap) => {
|
||||
export const swapStrings = (
|
||||
string: string,
|
||||
start: number,
|
||||
length: number,
|
||||
swap: string
|
||||
) => {
|
||||
return string.slice(0, start) + swap + string.slice(start + length)
|
||||
}
|
||||
|
||||
module.exports.removeHandlebarsStatements = (
|
||||
string,
|
||||
export const removeHandlebarsStatements = (
|
||||
string: string,
|
||||
replacement = "Invalid binding"
|
||||
) => {
|
||||
let regexp = new RegExp(exports.FIND_HBS_REGEX)
|
||||
let regexp = new RegExp(FIND_HBS_REGEX)
|
||||
let matches = string.match(regexp)
|
||||
if (matches == null) {
|
||||
return string
|
||||
}
|
||||
for (let match of matches) {
|
||||
const idx = string.indexOf(match)
|
||||
string = exports.swapStrings(string, idx, match.length, replacement)
|
||||
string = swapStrings(string, idx, match.length, replacement)
|
||||
}
|
||||
return string
|
||||
}
|
||||
|
||||
module.exports.btoa = plainText => {
|
||||
export const btoa = (plainText: string) => {
|
||||
return Buffer.from(plainText, "utf-8").toString("base64")
|
||||
}
|
||||
|
||||
module.exports.atob = base64 => {
|
||||
export const atob = (base64: string) => {
|
||||
return Buffer.from(base64, "base64").toString("utf-8")
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
const {
|
||||
import {
|
||||
processObject,
|
||||
processString,
|
||||
isValid,
|
||||
|
@ -8,7 +8,7 @@ const {
|
|||
doesContainString,
|
||||
disableEscaping,
|
||||
findHBSBlocks,
|
||||
} = require("../src/index.js")
|
||||
} from "../src/index"
|
||||
|
||||
describe("Test that the string processing works correctly", () => {
|
||||
it("should process a basic template string", async () => {
|
||||
|
@ -28,7 +28,7 @@ describe("Test that the string processing works correctly", () => {
|
|||
it("should fail gracefully when wrong type passed in", async () => {
|
||||
let error = null
|
||||
try {
|
||||
await processString(null, null)
|
||||
await processString(null as any, null as any)
|
||||
} catch (err) {
|
||||
error = err
|
||||
}
|
||||
|
@ -76,7 +76,7 @@ describe("Test that the object processing works correctly", () => {
|
|||
it("should fail gracefully when object passed in has cycles", async () => {
|
||||
let error = null
|
||||
try {
|
||||
const innerObj = { a: "thing {{ a }}" }
|
||||
const innerObj: any = { a: "thing {{ a }}" }
|
||||
innerObj.b = innerObj
|
||||
await processObject(innerObj, { a: 1 })
|
||||
} catch (err) {
|
||||
|
@ -98,7 +98,7 @@ describe("Test that the object processing works correctly", () => {
|
|||
it("should be able to handle null objects", async () => {
|
||||
let error = null
|
||||
try {
|
||||
await processObject(null, null)
|
||||
await processObject(null as any, null as any)
|
||||
} catch (err) {
|
||||
error = err
|
||||
}
|
|
@ -1,2 +1,2 @@
|
|||
module.exports.UUID_REGEX =
|
||||
export const UUID_REGEX =
|
||||
/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i
|
|
@ -1,4 +1,4 @@
|
|||
const { processString } = require("../src/index.js")
|
||||
import { processString } from "../src/index"
|
||||
|
||||
describe("Handling context properties with spaces in their name", () => {
|
||||
it("should allow through literal specifiers", async () => {
|
|
@ -1,6 +1,6 @@
|
|||
const { convertToJS } = require("../src/index.js")
|
||||
import { convertToJS } from "../src/index"
|
||||
|
||||
function checkLines(response, lines) {
|
||||
function checkLines(response: string, lines: string[]) {
|
||||
const toCheck = response.split("\n")
|
||||
let count = 0
|
||||
for (let line of lines) {
|
|
@ -1,7 +1,7 @@
|
|||
const { processString, processObject, isValid } = require("../src/index.js")
|
||||
const tableJson = require("./examples/table.json")
|
||||
const dayjs = require("dayjs")
|
||||
const { UUID_REGEX } = require("./constants")
|
||||
import { processString, processObject, isValid } from "../src/index"
|
||||
import tableJson from "./examples/table.json"
|
||||
import dayjs from "dayjs"
|
||||
import { UUID_REGEX } from "./constants"
|
||||
|
||||
describe("test the custom helpers we have applied", () => {
|
||||
it("should be able to use the object helper", async () => {
|
||||
|
@ -188,9 +188,7 @@ describe("test the date helpers", () => {
|
|||
time: date.toUTCString(),
|
||||
}
|
||||
)
|
||||
const formatted = new dayjs(date)
|
||||
.tz("America/New_York")
|
||||
.format("HH-mm-ss Z")
|
||||
const formatted = dayjs(date).tz("America/New_York").format("HH-mm-ss Z")
|
||||
expect(output).toBe(formatted)
|
||||
})
|
||||
|
||||
|
@ -200,7 +198,7 @@ describe("test the date helpers", () => {
|
|||
time: date.toUTCString(),
|
||||
})
|
||||
const timezone = dayjs.tz.guess()
|
||||
const offset = new dayjs(date).tz(timezone).format("Z")
|
||||
const offset = dayjs(date).tz(timezone).format("Z")
|
||||
expect(output).toBe(offset)
|
||||
})
|
||||
})
|
||||
|
@ -273,7 +271,7 @@ describe("test the string helpers", () => {
|
|||
})
|
||||
|
||||
describe("test the comparison helpers", () => {
|
||||
async function compare(func, a, b) {
|
||||
async function compare(func: string, a: any, b: any) {
|
||||
const output = await processString(
|
||||
`{{ #${func} a b }}Success{{ else }}Fail{{ /${func} }}`,
|
||||
{
|
||||
|
@ -344,14 +342,14 @@ describe("Test the literal helper", () => {
|
|||
})
|
||||
|
||||
it("should allow use of the literal specifier for an object", async () => {
|
||||
const output = await processString(`{{literal a}}`, {
|
||||
const output: any = await processString(`{{literal a}}`, {
|
||||
a: { b: 1 },
|
||||
})
|
||||
expect(output.b).toBe(1)
|
||||
})
|
||||
|
||||
it("should allow use of the literal specifier for an object with dashes", async () => {
|
||||
const output = await processString(`{{literal a}}`, {
|
||||
const output: any = await processString(`{{literal a}}`, {
|
||||
a: { b: "i-have-dashes" },
|
||||
})
|
||||
expect(output.b).toBe("i-have-dashes")
|
|
@ -1,13 +1,9 @@
|
|||
const vm = require("vm")
|
||||
import vm from "vm"
|
||||
|
||||
const {
|
||||
processStringSync,
|
||||
encodeJSBinding,
|
||||
setJSRunner,
|
||||
} = require("../src/index.js")
|
||||
const { UUID_REGEX } = require("./constants")
|
||||
import { processStringSync, encodeJSBinding, setJSRunner } from "../src/index"
|
||||
import { UUID_REGEX } from "./constants"
|
||||
|
||||
const processJS = (js, context) => {
|
||||
const processJS = (js: string, context?: object): any => {
|
||||
return processStringSync(encodeJSBinding(js), context)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
const vm = require("vm")
|
||||
import vm from "vm"
|
||||
|
||||
jest.mock("@budibase/handlebars-helpers/lib/math", () => {
|
||||
const actual = jest.requireActual("@budibase/handlebars-helpers/lib/math")
|
||||
|
@ -17,14 +17,14 @@ jest.mock("@budibase/handlebars-helpers/lib/uuid", () => {
|
|||
}
|
||||
})
|
||||
|
||||
const { processString, setJSRunner } = require("../src/index.js")
|
||||
import { processString, setJSRunner } from "../src/index"
|
||||
|
||||
const tk = require("timekeeper")
|
||||
const { getParsedManifest, runJsHelpersTests } = require("./utils")
|
||||
import tk from "timekeeper"
|
||||
import { getParsedManifest, runJsHelpersTests } from "./utils"
|
||||
|
||||
tk.freeze("2021-01-21T12:00:00")
|
||||
|
||||
function escapeRegExp(string) {
|
||||
function escapeRegExp(string: string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") // $& means the whole matched string
|
||||
}
|
||||
|
||||
|
@ -40,9 +40,9 @@ describe("manifest", () => {
|
|||
describe("examples are valid", () => {
|
||||
describe.each(Object.keys(manifest))("%s", collection => {
|
||||
it.each(manifest[collection])("%s", async (_, { hbs, js }) => {
|
||||
const context = {
|
||||
double: i => i * 2,
|
||||
isString: x => typeof x === "string",
|
||||
const context: any = {
|
||||
double: (i: number) => i * 2,
|
||||
isString: (x: any) => typeof x === "string",
|
||||
}
|
||||
|
||||
const arrays = hbs.match(/\[[^/\]]+\]/)
|
|
@ -1,4 +1,4 @@
|
|||
const { processString } = require("../src/index.js")
|
||||
import { processString } from "../src/index"
|
||||
|
||||
describe("specific test case for whether or not full app template can still be rendered", () => {
|
||||
it("should be able to render the app template", async () => {
|
|
@ -1,13 +1,9 @@
|
|||
const { getManifest } = require("../src")
|
||||
const { getJsHelperList } = require("../src/helpers")
|
||||
import { getManifest } from "../src"
|
||||
import { getJsHelperList } from "../src/helpers"
|
||||
|
||||
const {
|
||||
convertToJS,
|
||||
processStringSync,
|
||||
encodeJSBinding,
|
||||
} = require("../src/index.js")
|
||||
import { convertToJS, processStringSync, encodeJSBinding } from "../src/index"
|
||||
|
||||
function tryParseJson(str) {
|
||||
function tryParseJson(str: string) {
|
||||
if (typeof str !== "string") {
|
||||
return
|
||||
}
|
||||
|
@ -19,23 +15,35 @@ function tryParseJson(str) {
|
|||
}
|
||||
}
|
||||
|
||||
const getParsedManifest = () => {
|
||||
const manifest = getManifest()
|
||||
type ExampleType = [
|
||||
string,
|
||||
{
|
||||
hbs: string
|
||||
js: string
|
||||
requiresHbsBody: boolean
|
||||
}
|
||||
]
|
||||
|
||||
export const getParsedManifest = () => {
|
||||
const manifest: any = getManifest()
|
||||
const collections = Object.keys(manifest)
|
||||
|
||||
const examples = collections.reduce((acc, collection) => {
|
||||
const functions = Object.entries(manifest[collection])
|
||||
.filter(([_, details]) => details.example)
|
||||
.map(([name, details]) => {
|
||||
const functions = Object.entries<{
|
||||
example: string
|
||||
requiresBlock: boolean
|
||||
}>(manifest[collection])
|
||||
.filter(
|
||||
([_, details]) =>
|
||||
details.example?.split("->").map(x => x.trim()).length > 1
|
||||
)
|
||||
.map(([name, details]): ExampleType => {
|
||||
const example = details.example
|
||||
let [hbs, js] = example.split("->").map(x => x.trim())
|
||||
if (!js) {
|
||||
// The function has no return value
|
||||
return
|
||||
}
|
||||
|
||||
// Trim 's
|
||||
js = js.replace(/^'|'$/g, "")
|
||||
let parsedExpected
|
||||
let parsedExpected: string
|
||||
if ((parsedExpected = tryParseJson(js))) {
|
||||
if (Array.isArray(parsedExpected)) {
|
||||
if (typeof parsedExpected[0] === "object") {
|
||||
|
@ -48,36 +56,40 @@ const getParsedManifest = () => {
|
|||
const requiresHbsBody = details.requiresBlock
|
||||
return [name, { hbs, js, requiresHbsBody }]
|
||||
})
|
||||
.filter(x => !!x)
|
||||
|
||||
if (Object.keys(functions).length) {
|
||||
if (functions.length) {
|
||||
acc[collection] = functions
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
}, {} as Record<string, ExampleType[]>)
|
||||
|
||||
return examples
|
||||
}
|
||||
module.exports.getParsedManifest = getParsedManifest
|
||||
|
||||
module.exports.runJsHelpersTests = ({ funcWrap, testsToSkip } = {}) => {
|
||||
funcWrap = funcWrap || (delegate => delegate())
|
||||
export const runJsHelpersTests = ({
|
||||
funcWrap,
|
||||
testsToSkip,
|
||||
}: {
|
||||
funcWrap?: any
|
||||
testsToSkip?: any
|
||||
} = {}) => {
|
||||
funcWrap = funcWrap || ((delegate: () => any) => delegate())
|
||||
const manifest = getParsedManifest()
|
||||
|
||||
const processJS = (js, context) => {
|
||||
const processJS = (js: string, context: object | undefined) => {
|
||||
return funcWrap(() => processStringSync(encodeJSBinding(js), context))
|
||||
}
|
||||
|
||||
function escapeRegExp(string) {
|
||||
function escapeRegExp(string: string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") // $& means the whole matched string
|
||||
}
|
||||
|
||||
describe("can be parsed and run as js", () => {
|
||||
const jsHelpers = getJsHelperList()
|
||||
const jsHelpers = getJsHelperList()!
|
||||
const jsExamples = Object.keys(manifest).reduce((acc, v) => {
|
||||
acc[v] = manifest[v].filter(([key]) => jsHelpers[key])
|
||||
return acc
|
||||
}, {})
|
||||
}, {} as typeof manifest)
|
||||
|
||||
describe.each(Object.keys(jsExamples))("%s", collection => {
|
||||
const examplesToRun = jsExamples[collection]
|
||||
|
@ -86,9 +98,9 @@ module.exports.runJsHelpersTests = ({ funcWrap, testsToSkip } = {}) => {
|
|||
|
||||
examplesToRun.length &&
|
||||
it.each(examplesToRun)("%s", async (_, { hbs, js }) => {
|
||||
const context = {
|
||||
double: i => i * 2,
|
||||
isString: x => typeof x === "string",
|
||||
const context: any = {
|
||||
double: (i: number) => i * 2,
|
||||
isString: (x: any) => typeof x === "string",
|
||||
}
|
||||
|
||||
const arrays = hbs.match(/\[[^/\]]+\]/)
|
|
@ -5,8 +5,10 @@ jest.mock("../src/utilities", () => {
|
|||
isBackendService: jest.fn().mockReturnValue(true),
|
||||
}
|
||||
})
|
||||
const { defaultJSSetup, processStringSync, encodeJSBinding } = require("../src")
|
||||
const { isBackendService } = require("../src/utilities")
|
||||
|
||||
import { defaultJSSetup, processStringSync, encodeJSBinding } from "../src"
|
||||
import { isBackendService } from "../src/utilities"
|
||||
|
||||
const mockedBackendService = jest.mocked(isBackendService)
|
||||
|
||||
const binding = encodeJSBinding("return 1")
|
|
@ -1,11 +1,15 @@
|
|||
{
|
||||
"include": ["src/**/*"],
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"target": "es6",
|
||||
"moduleResolution": "node",
|
||||
"noImplicitAny": true,
|
||||
"incremental": true,
|
||||
"lib": ["dom"],
|
||||
"outDir": "dist",
|
||||
"esModuleInterop": true,
|
||||
"types": ["node", "jest"]
|
||||
"types": ["node", "jest"],
|
||||
"resolveJsonModule": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,17 +16,11 @@ COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.
|
|||
RUN chmod +x ./scripts/removeWorkspaceDependencies.sh
|
||||
|
||||
|
||||
WORKDIR /string-templates
|
||||
COPY packages/string-templates/package.json package.json
|
||||
RUN ../scripts/removeWorkspaceDependencies.sh package.json
|
||||
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true --network-timeout 1000000
|
||||
COPY packages/string-templates .
|
||||
|
||||
|
||||
WORKDIR /app
|
||||
COPY packages/worker/package.json .
|
||||
COPY packages/worker/dist/yarn.lock .
|
||||
RUN cd ../string-templates && yarn link && cd - && yarn link @budibase/string-templates
|
||||
|
||||
RUN ../scripts/removeWorkspaceDependencies.sh package.json
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ const config: Config.InitialOptions = {
|
|||
"@budibase/backend-core": "<rootDir>/../backend-core/src",
|
||||
"@budibase/types": "<rootDir>/../types/src",
|
||||
"@budibase/shared-core": ["<rootDir>/../shared-core/src"],
|
||||
"@budibase/string-templates": ["<rootDir>/../string-templates/src"],
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -114,9 +114,16 @@ export const syncAppFavourites = async (processedAppIds: string[]) => {
|
|||
if (processedAppIds.length === 0) {
|
||||
return []
|
||||
}
|
||||
const apps = await fetchAppsByIds(processedAppIds)
|
||||
|
||||
const tenantId = tenancy.getTenantId()
|
||||
const appPrefix =
|
||||
tenantId === tenancy.DEFAULT_TENANT_ID
|
||||
? dbCore.APP_DEV_PREFIX
|
||||
: `${dbCore.APP_DEV_PREFIX}${tenantId}_`
|
||||
|
||||
const apps = await fetchAppsByIds(processedAppIds, appPrefix)
|
||||
return apps?.reduce((acc: string[], app) => {
|
||||
const id = app.appId.replace(dbCore.APP_DEV_PREFIX, "")
|
||||
const id = app.appId.replace(appPrefix, "")
|
||||
if (processedAppIds.includes(id)) {
|
||||
acc.push(id)
|
||||
}
|
||||
|
@ -124,9 +131,14 @@ export const syncAppFavourites = async (processedAppIds: string[]) => {
|
|||
}, [])
|
||||
}
|
||||
|
||||
export const fetchAppsByIds = async (processedAppIds: string[]) => {
|
||||
export const fetchAppsByIds = async (
|
||||
processedAppIds: string[],
|
||||
appPrefix: string
|
||||
) => {
|
||||
return await dbCore.getAppsByIDs(
|
||||
processedAppIds.map(appId => `${dbCore.APP_DEV_PREFIX}${appId}`)
|
||||
processedAppIds.map(appId => {
|
||||
return `${appPrefix}${appId}`
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,8 @@
|
|||
"@budibase/backend-core": ["../backend-core/src"],
|
||||
"@budibase/backend-core/*": ["../backend-core/*"],
|
||||
"@budibase/shared-core": ["../shared-core/src"],
|
||||
"@budibase/pro": ["../pro/src"]
|
||||
"@budibase/pro": ["../pro/src"],
|
||||
"@budibase/string-templates": ["../string-templates/src"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
|
|
48
yarn.lock
48
yarn.lock
|
@ -1988,7 +1988,7 @@
|
|||
resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310"
|
||||
integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==
|
||||
|
||||
"@babel/runtime@^7.10.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.21.0":
|
||||
"@babel/runtime@^7.10.5", "@babel/runtime@^7.13.10":
|
||||
version "7.23.9"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.9.tgz#47791a15e4603bb5f905bc0753801cf21d6345f7"
|
||||
integrity sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==
|
||||
|
@ -8467,21 +8467,6 @@ concat-with-sourcemaps@^1.1.0:
|
|||
dependencies:
|
||||
source-map "^0.6.1"
|
||||
|
||||
concurrently@^8.2.2:
|
||||
version "8.2.2"
|
||||
resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-8.2.2.tgz#353141985c198cfa5e4a3ef90082c336b5851784"
|
||||
integrity sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==
|
||||
dependencies:
|
||||
chalk "^4.1.2"
|
||||
date-fns "^2.30.0"
|
||||
lodash "^4.17.21"
|
||||
rxjs "^7.8.1"
|
||||
shell-quote "^1.8.1"
|
||||
spawn-command "0.0.2"
|
||||
supports-color "^8.1.1"
|
||||
tree-kill "^1.2.2"
|
||||
yargs "^17.7.2"
|
||||
|
||||
condense-newlines@^0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/condense-newlines/-/condense-newlines-0.2.1.tgz#3de985553139475d32502c83b02f60684d24c55f"
|
||||
|
@ -9049,13 +9034,6 @@ data-urls@^4.0.0:
|
|||
whatwg-mimetype "^3.0.0"
|
||||
whatwg-url "^12.0.0"
|
||||
|
||||
date-fns@^2.30.0:
|
||||
version "2.30.0"
|
||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0"
|
||||
integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.21.0"
|
||||
|
||||
dateformat@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
|
||||
|
@ -19400,13 +19378,6 @@ rxjs@^7.5.5:
|
|||
dependencies:
|
||||
tslib "^2.1.0"
|
||||
|
||||
rxjs@^7.8.1:
|
||||
version "7.8.1"
|
||||
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543"
|
||||
integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==
|
||||
dependencies:
|
||||
tslib "^2.1.0"
|
||||
|
||||
safe-array-concat@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.1.tgz#91686a63ce3adbea14d61b14c99572a8ff84754c"
|
||||
|
@ -19682,11 +19653,6 @@ shell-exec@1.0.2:
|
|||
resolved "https://registry.yarnpkg.com/shell-exec/-/shell-exec-1.0.2.tgz#2e9361b0fde1d73f476c4b6671fa17785f696756"
|
||||
integrity sha512-jyVd+kU2X+mWKMmGhx4fpWbPsjvD53k9ivqetutVW/BQ+WIZoDoP4d8vUMGezV6saZsiNoW2f9GIhg9Dondohg==
|
||||
|
||||
shell-quote@^1.8.1:
|
||||
version "1.8.1"
|
||||
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680"
|
||||
integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==
|
||||
|
||||
shortid@2.2.15:
|
||||
version "2.2.15"
|
||||
resolved "https://registry.yarnpkg.com/shortid/-/shortid-2.2.15.tgz#2b902eaa93a69b11120373cd42a1f1fe4437c122"
|
||||
|
@ -20005,11 +19971,6 @@ sparse-bitfield@^3.0.3:
|
|||
dependencies:
|
||||
memory-pager "^1.0.2"
|
||||
|
||||
spawn-command@0.0.2:
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2.tgz#9544e1a43ca045f8531aac1a48cb29bdae62338e"
|
||||
integrity sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==
|
||||
|
||||
spdx-correct@^3.0.0:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
|
||||
|
@ -20582,7 +20543,7 @@ supports-color@^7.0.0, supports-color@^7.1.0:
|
|||
dependencies:
|
||||
has-flag "^4.0.0"
|
||||
|
||||
supports-color@^8.0.0, supports-color@^8.1.1:
|
||||
supports-color@^8.0.0:
|
||||
version "8.1.1"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
|
||||
integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
|
||||
|
@ -21209,11 +21170,6 @@ tr46@~0.0.3:
|
|||
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
|
||||
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
|
||||
|
||||
tree-kill@^1.2.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
|
||||
integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
|
||||
|
||||
trim-newlines@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144"
|
||||
|
|
Loading…
Reference in New Issue