diff --git a/.github/workflows/stale_bot.yml b/.github/workflows/stale_bot.yml index 411a70a463..df222a8483 100644 --- a/.github/workflows/stale_bot.yml +++ b/.github/workflows/stale_bot.yml @@ -8,41 +8,15 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v8 - with: - days-before-stale: 330 - operations-per-run: 1 - # stale rules for PRs - days-before-pr-stale: 7 - stale-issue-label: stale - exempt-pr-labels: pinned,security,roadmap - days-before-pr-close: 7 - days-before-issue-close: 30 - - - uses: actions/stale@v8 - with: - operations-per-run: 3 - # stale rules for high priority bugs - days-before-stale: 30 - only-issue-labels: bug,High priority - stale-issue-label: warn - days-before-close: 30 - - - uses: actions/stale@v8 - with: - operations-per-run: 3 - # stale rules for medium priority bugs - days-before-stale: 90 - only-issue-labels: bug,Medium priority - stale-issue-label: warn - days-before-close: 30 - - - uses: actions/stale@v8 - with: - operations-per-run: 3 - # stale rules for all bugs - days-before-stale: 180 - stale-issue-label: stale - only-issue-labels: bug - stale-issue-message: "This issue has been automatically marked as stale because it has not had any activity for six months." - days-before-close: 30 + - uses: actions/stale@v8 + with: + # Issues + days-before-stale: 180 + stale-issue-label: stale + days-before-close: 30 + stale-issue-message: "This issue has been automatically marked as stale as there has been no activity for 6 months." + # Pull requests + days-before-pr-stale: 7 + days-before-pr-close: 14 + exempt-pr-labels: pinned,security,roadmap + operations-per-run: 100 diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index e5cd54e5a5..db5fcbaebb 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -2043,6 +2043,101 @@ if (descriptions.length) { expect(rows[0].name).toEqual("Clare updated") expect(rows[1].name).toEqual("Jeff updated") }) + + it("should reject bulkImport date only fields with wrong format", async () => { + const table = await config.api.table.save( + saveTableRequest({ + schema: { + date: { + type: FieldType.DATETIME, + dateOnly: true, + name: "date", + }, + }, + }) + ) + + await config.api.row.bulkImport( + table._id!, + { + rows: [ + { + date: "01.02.2024", + }, + ], + }, + { + status: 400, + body: { + message: + 'Invalid format for field "date": "01.02.2024". Date-only fields must be in the format "YYYY-MM-DD".', + }, + } + ) + }) + + it("should reject bulkImport date time fields with wrong format", async () => { + const table = await config.api.table.save( + saveTableRequest({ + schema: { + date: { + type: FieldType.DATETIME, + name: "date", + }, + }, + }) + ) + + await config.api.row.bulkImport( + table._id!, + { + rows: [ + { + date: "01.02.2024", + }, + ], + }, + { + status: 400, + body: { + message: + 'Invalid format for field "date": "01.02.2024". Datetime fields must be in ISO format, e.g. "YYYY-MM-DDTHH:MM:SSZ".', + }, + } + ) + }) + + it("should reject bulkImport time fields with wrong format", async () => { + const table = await config.api.table.save( + saveTableRequest({ + schema: { + time: { + type: FieldType.DATETIME, + timeOnly: true, + name: "time", + }, + }, + }) + ) + + await config.api.row.bulkImport( + table._id!, + { + rows: [ + { + time: "3pm", + }, + ], + }, + { + status: 400, + body: { + message: + 'Invalid format for field "time": "3pm". Time-only fields must be in the format "HH:MM:SS".', + }, + } + ) + }) }) describe("enrich", () => { diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index 4de92f21e5..c3b274d5f4 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -1705,7 +1705,10 @@ if (descriptions.length) { beforeAll(async () => { tableOrViewId = await createTableOrView({ - dateid: { name: "dateid", type: FieldType.STRING }, + dateid: { + name: "dateid", + type: FieldType.STRING, + }, date: { name: "date", type: FieldType.DATETIME, @@ -1751,7 +1754,9 @@ if (descriptions.length) { describe("notEqual", () => { it("successfully finds a row", async () => { await expectQuery({ - notEqual: { date: `${JAN_1ST}${SEARCH_SUFFIX}` }, + notEqual: { + date: `${JAN_1ST}${SEARCH_SUFFIX}`, + }, }).toContainExactly([ { date: JAN_10TH }, { dateid: NULL_DATE__ID }, @@ -1760,7 +1765,9 @@ if (descriptions.length) { it("fails to find nonexistent row", async () => { await expectQuery({ - notEqual: { date: `${JAN_30TH}${SEARCH_SUFFIX}` }, + notEqual: { + date: `${JAN_30TH}${SEARCH_SUFFIX}`, + }, }).toContainExactly([ { date: JAN_1ST }, { date: JAN_10TH }, @@ -1822,6 +1829,60 @@ if (descriptions.length) { }).toFindNothing() }) }) + + describe("sort", () => { + it("sorts ascending", async () => { + await expectSearch({ + query: {}, + sort: "date", + sortOrder: SortOrder.ASCENDING, + }).toMatchExactly([ + { dateid: NULL_DATE__ID }, + { date: JAN_1ST }, + { date: JAN_10TH }, + ]) + }) + + it("sorts descending", async () => { + await expectSearch({ + query: {}, + sort: "date", + sortOrder: SortOrder.DESCENDING, + }).toMatchExactly([ + { date: JAN_10TH }, + { date: JAN_1ST }, + { dateid: NULL_DATE__ID }, + ]) + }) + + describe("sortType STRING", () => { + it("sorts ascending", async () => { + await expectSearch({ + query: {}, + sort: "date", + sortType: SortType.STRING, + sortOrder: SortOrder.ASCENDING, + }).toMatchExactly([ + { dateid: NULL_DATE__ID }, + { date: JAN_1ST }, + { date: JAN_10TH }, + ]) + }) + + it("sorts descending", async () => { + await expectSearch({ + query: {}, + sort: "date", + sortType: SortType.STRING, + sortOrder: SortOrder.DESCENDING, + }).toMatchExactly([ + { date: JAN_10TH }, + { date: JAN_1ST }, + { dateid: NULL_DATE__ID }, + ]) + }) + }) + }) } ) } diff --git a/packages/server/src/utilities/schema.ts b/packages/server/src/utilities/schema.ts index cfdd0d753a..b13999c842 100644 --- a/packages/server/src/utilities/schema.ts +++ b/packages/server/src/utilities/schema.ts @@ -7,7 +7,7 @@ import { Table, } from "@budibase/types" import { ValidColumnNameRegex, helpers, utils } from "@budibase/shared-core" -import { db } from "@budibase/backend-core" +import { db, HTTPError, sql } from "@budibase/backend-core" type Rows = Array @@ -175,15 +175,27 @@ export function parse(rows: Rows, table: Table): Rows { if ([FieldType.NUMBER].includes(columnType)) { // If provided must be a valid number parsedRow[columnName] = columnData ? Number(columnData) : columnData - } else if ( - columnType === FieldType.DATETIME && - !columnSchema.timeOnly && - !columnSchema.dateOnly - ) { - // If provided must be a valid date + } else if (columnType === FieldType.DATETIME) { + if (columnData && !columnSchema.timeOnly) { + if (!sql.utils.isValidISODateString(columnData)) { + let message = `Invalid format for field "${columnName}": "${columnData}".` + if (columnSchema.dateOnly) { + message += ` Date-only fields must be in the format "YYYY-MM-DD".` + } else { + message += ` Datetime fields must be in ISO format, e.g. "YYYY-MM-DDTHH:MM:SSZ".` + } + throw new HTTPError(message, 400) + } + } + if (columnData && columnSchema.timeOnly) { + if (!sql.utils.isValidTime(columnData)) { + throw new HTTPError( + `Invalid format for field "${columnName}": "${columnData}". Time-only fields must be in the format "HH:MM:SS".`, + 400 + ) + } + } parsedRow[columnName] = columnData - ? new Date(columnData).toISOString() - : columnData } else if ( columnType === FieldType.JSON && typeof columnData === "string"