diff --git a/packages/bbui/src/Form/Core/DatePicker.svelte b/packages/bbui/src/Form/Core/DatePicker.svelte index 73ba7bb642..39a7d9d626 100644 --- a/packages/bbui/src/Form/Core/DatePicker.svelte +++ b/packages/bbui/src/Form/Core/DatePicker.svelte @@ -15,6 +15,7 @@ export let placeholder = null export let appendTo = undefined export let timeOnly = false + export let ignoreTimezones = false const dispatch = createEventDispatcher() const flatpickrId = `${uuid()}-wrapper` @@ -50,19 +51,35 @@ const handleChange = event => { const [dates] = event.detail + const noTimezone = enableTime && !timeOnly && ignoreTimezones let newValue = dates[0] if (newValue) { newValue = newValue.toISOString() } - // if time only set date component to 2000-01-01 + + // If time only set date component to 2000-01-01 if (timeOnly) { newValue = `2000-01-01T${newValue.split("T")[1]}` } - // date only, offset for timezone so always right date + + // For date-only fields, construct a manual timestamp string without a time + // or time zone else if (!enableTime) { - const offset = dates[0].getTimezoneOffset() * 60000 - newValue = new Date(dates[0].getTime() - offset).toISOString() + const year = dates[0].getFullYear() + const month = `${dates[0].getMonth() + 1}`.padStart(2, "0") + const day = `${dates[0].getDate()}`.padStart(2, "0") + newValue = `${year}-${month}-${day}T00:00:00.000` } + + // For non-timezone-aware fields, create an ISO 8601 timestamp of the exact + // time picked, without timezone + else if (noTimezone) { + const offset = dates[0].getTimezoneOffset() * 60000 + newValue = new Date(dates[0].getTime() - offset) + .toISOString() + .slice(0, -1) + } + dispatch("change", newValue) } @@ -112,10 +129,12 @@ // Treat as numerical timestamp date = new Date(parseInt(val)) } + time = date.getTime() if (isNaN(time)) { return null } + // By rounding to the nearest second we avoid locking up in an endless // loop in the builder, caused by potentially enriching {{ now }} to every // millisecond. diff --git a/packages/bbui/src/Form/DatePicker.svelte b/packages/bbui/src/Form/DatePicker.svelte index 9298c49177..a4b2379782 100644 --- a/packages/bbui/src/Form/DatePicker.svelte +++ b/packages/bbui/src/Form/DatePicker.svelte @@ -12,6 +12,7 @@ export let timeOnly = false export let placeholder = null export let appendTo = undefined + export let ignoreTimezones = false const dispatch = createEventDispatcher() @@ -30,6 +31,7 @@ {enableTime} {timeOnly} {appendTo} + {ignoreTimezones} on:change={onChange} /> diff --git a/packages/bbui/src/Tooltip/TooltipWrapper.svelte b/packages/bbui/src/Tooltip/TooltipWrapper.svelte index 78c69942e5..92f5c6f474 100644 --- a/packages/bbui/src/Tooltip/TooltipWrapper.svelte +++ b/packages/bbui/src/Tooltip/TooltipWrapper.svelte @@ -39,7 +39,6 @@ position: relative; display: flex; justify-content: center; - margin-top: 1px; margin-left: 5px; margin-right: 5px; } diff --git a/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js b/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js index e748161529..9176d535ab 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js +++ b/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js @@ -170,28 +170,29 @@ export function makeDatasourceFormComponents(datasource) { optionsType: "select", optionsSource: "schema", }) - } - if (fieldType === "longform") { + } else if (fieldType === "longform") { component.customProps({ format: "auto", }) - } - if (fieldType === "array") { + } else if (fieldType === "array") { component.customProps({ placeholder: "Choose an option", optionsSource: "schema", }) - } - - if (fieldType === "link") { + } else if (fieldType === "link") { let placeholder = fieldSchema.relationshipType === "one-to-many" ? "Choose an option" : "Choose some options" component.customProps({ placeholder }) - } - if (fieldType === "boolean") { + } else if (fieldType === "boolean") { component.customProps({ text: field, label: "" }) + } else if (fieldType === "datetime") { + component.customProps({ + enableTime: !fieldSchema?.dateOnly, + timeOnly: fieldSchema?.timeOnly, + ignoreTimezones: fieldSchema.ignoreTimezones, + }) } components.push(component) } diff --git a/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte b/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte index 2286ae82aa..ab18f744fc 100644 --- a/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte +++ b/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte @@ -53,6 +53,7 @@ {label} timeOnly={isTimeStamp} enableTime={!meta?.dateOnly} + ignoreTimezones={meta?.ignoreTimezones} bind:value /> {:else if type === "attachment"} diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte index 62a367ea7d..77ab75827f 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte @@ -14,7 +14,7 @@ } from "@budibase/bbui" import { createEventDispatcher, onMount } from "svelte" import { cloneDeep } from "lodash/fp" - import { tables } from "stores/backend" + import { tables, datasources } from "stores/backend" import { TableNames, UNEDITABLE_USER_FIELDS } from "constants" import { FIELDS, @@ -63,6 +63,7 @@ let primaryDisplay = $tables.selected.primaryDisplay == null || $tables.selected.primaryDisplay === field.name + let isCreating = originalName == null let table = $tables.selected let indexes = [...($tables.selected.indexes || [])] @@ -81,6 +82,9 @@ (field.type === LINK_TYPE && !field.tableId) || Object.keys(errors).length !== 0 $: errors = checkErrors(field) + $: datasource = $datasources.list.find( + source => source._id === table?.sourceId + ) // used to select what different options can be displayed for column type $: canBeSearched = @@ -430,6 +434,18 @@ bind:value={field.constraints.datetime.earliest} /> + {#if datasource?.source !== "ORACLE" && datasource?.source !== "SQL_SERVER"} +
+ + +
+ {/if} {:else if field.type === "number"} {/if} diff --git a/packages/server/src/api/controllers/row/ExternalRequest.ts b/packages/server/src/api/controllers/row/ExternalRequest.ts index c8c8ae8e58..7983044f66 100644 --- a/packages/server/src/api/controllers/row/ExternalRequest.ts +++ b/packages/server/src/api/controllers/row/ExternalRequest.ts @@ -29,7 +29,10 @@ import { breakExternalTableId, isSQL } from "../../../integrations/utils" import { processObjectSync } from "@budibase/string-templates" // @ts-ignore import { cloneDeep } from "lodash/fp" -import { processFormulas } from "../../../utilities/rowProcessor/utils" +import { + processFormulas, + processDates, +} from "../../../utilities/rowProcessor/utils" // @ts-ignore import { getAppDB } from "@budibase/backend-core/context" @@ -434,7 +437,13 @@ module External { relationships ) } - return processFormulas(table, Object.values(finalRows)).map((row: Row) => + + // Process some additional data types + let finalRowArray = Object.values(finalRows) + finalRowArray = processDates(table, finalRowArray) + finalRowArray = processFormulas(table, finalRowArray) + + return finalRowArray.map((row: Row) => this.squashRelationshipColumns(table, row, relationships) ) } diff --git a/packages/server/src/definitions/common.ts b/packages/server/src/definitions/common.ts index 3ee6a71c8f..4aec0d103d 100644 --- a/packages/server/src/definitions/common.ts +++ b/packages/server/src/definitions/common.ts @@ -25,6 +25,7 @@ export interface FieldSchema { formula?: string formulaType?: string main?: boolean + ignoreTimezones?: boolean meta?: { toTable: string toKey: string diff --git a/packages/server/src/integrations/base/sqlTable.ts b/packages/server/src/integrations/base/sqlTable.ts index 0c63b707ae..71f9c4aa64 100644 --- a/packages/server/src/integrations/base/sqlTable.ts +++ b/packages/server/src/integrations/base/sqlTable.ts @@ -61,7 +61,9 @@ function generateSchema( schema.boolean(key) break case FieldTypes.DATETIME: - schema.datetime(key) + schema.datetime(key, { + useTz: !column.ignoreTimezones, + }) break case FieldTypes.ARRAY: schema.json(key) diff --git a/packages/server/src/integrations/mysql.ts b/packages/server/src/integrations/mysql.ts index 4fe996a019..7a06592ef7 100644 --- a/packages/server/src/integrations/mysql.ts +++ b/packages/server/src/integrations/mysql.ts @@ -15,7 +15,6 @@ import { } from "./utils" import { DatasourcePlus } from "./base/datasourcePlus" import dayjs from "dayjs" -import { FieldTypes } from "../constants" const { NUMBER_REGEX } = require("../utilities") module MySQLModule { @@ -30,6 +29,7 @@ module MySQLModule { database: string ssl?: { [key: string]: any } rejectUnauthorized: boolean + typeCast: Function } const SCHEMA: Integration = { @@ -89,6 +89,8 @@ module MySQLModule { }, } + const TimezoneAwareDateTypes = ["timestamp"] + function bindingTypeCoerce(bindings: any[]) { for (let i = 0; i < bindings.length; i++) { const binding = bindings[i] @@ -131,7 +133,19 @@ module MySQLModule { } // @ts-ignore delete config.rejectUnauthorized - this.config = config + this.config = { + ...config, + typeCast: function (field: any, next: any) { + if ( + field.type == "DATETIME" || + field.type === "DATE" || + field.type === "TIMESTAMP" + ) { + return field.string() + } + return next() + }, + } } getBindingIdentifier(): string { diff --git a/packages/server/src/integrations/postgres.ts b/packages/server/src/integrations/postgres.ts index 220f35dae5..f0c5911476 100644 --- a/packages/server/src/integrations/postgres.ts +++ b/packages/server/src/integrations/postgres.ts @@ -16,10 +16,19 @@ import { import { DatasourcePlus } from "./base/datasourcePlus" module PostgresModule { - const { Client } = require("pg") + const { Client, types } = require("pg") const Sql = require("./base/sql") const { escapeDangerousCharacters } = require("../utilities") + // Return "date" and "timestamp" types as plain strings. + // This lets us reference the original stored timezone. + // types is undefined when running in a test env for some reason. + if (types) { + types.setTypeParser(1114, (val: any) => val) // timestamp + types.setTypeParser(1082, (val: any) => val) // date + types.setTypeParser(1184, (val: any) => val) // timestampz + } + const JSON_REGEX = /'{.*}'::json/s interface PostgresConfig { diff --git a/packages/server/src/utilities/rowProcessor/utils.js b/packages/server/src/utilities/rowProcessor/utils.js index 262ef40a3a..c80dae497c 100644 --- a/packages/server/src/utilities/rowProcessor/utils.js +++ b/packages/server/src/utilities/rowProcessor/utils.js @@ -65,3 +65,28 @@ exports.processFormulas = ( } return single ? rows[0] : rows } + +/** + * Processes any date columns and ensures that those without the ignoreTimezones + * flag set are parsed as UTC rather than local time. + */ +exports.processDates = (table, rows) => { + let datesWithTZ = [] + for (let [column, schema] of Object.entries(table.schema)) { + if (schema.type !== FieldTypes.DATETIME) { + continue + } + if (!schema.ignoreTimezones) { + datesWithTZ.push(column) + } + } + + for (let row of rows) { + for (let col of datesWithTZ) { + if (row[col] && typeof row[col] === "string" && !row[col].endsWith("Z")) { + row[col] = new Date(row[col]).toISOString() + } + } + } + return rows +} diff --git a/packages/server/yarn.lock b/packages/server/yarn.lock index c8dad37383..e8e1d3e269 100644 --- a/packages/server/yarn.lock +++ b/packages/server/yarn.lock @@ -1014,10 +1014,10 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@1.0.194": - version "1.0.194" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.0.194.tgz#08b2b1aec3c88efbc7cfb14145ce6f199475c538" - integrity sha512-BbnJFtAioJeD9tQfSwc2uftFK8SagREgSfUYv06dfnf/NsmkrONzZiTon1oA57S7ifcSiu+WZf87lNX0k8pwfQ== +"@budibase/backend-core@1.0.195": + version "1.0.195" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.0.195.tgz#ee40c690ae4a54febab8b140c9bbb7d04221f3b9" + integrity sha512-6diWgRV9t4DU3kXteJJAhCxyta9m1wvzN7vNbflhY4kYJeg7BC+7jcvc2r8zl6s1vVeSW4ic5/ueSLRaTDySuw== dependencies: "@techpass/passport-openidconnect" "^0.3.0" aws-sdk "^2.901.0" @@ -1092,12 +1092,12 @@ svelte-flatpickr "^3.2.3" svelte-portal "^1.0.0" -"@budibase/pro@1.0.194": - version "1.0.194" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.0.194.tgz#fbf977b292b9a6dbf7b072b2e0379dcf4379943a" - integrity sha512-LSqVwlhKWwFNnC3acvLnCzbeBoze1Ma6GELE/b5ZxS65owsigu0KC6T/UuujEbU9xqbFJ3R6uV+4Fz4NUibLIw== +"@budibase/pro@1.0.195": + version "1.0.195" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.0.195.tgz#368652398d1da95f160fc0192b77144b11147ff5" + integrity sha512-zf1f1exHop4m6vda5hObUnTZa2PIBRnc4e0r9iqFbzvGBMfBLGUhGzu23JEwNYaS2xhWHj2FNv4/IVzIyLG4eA== dependencies: - "@budibase/backend-core" "1.0.194" + "@budibase/backend-core" "1.0.195" node-fetch "^2.6.1" "@budibase/standard-components@^0.9.139": diff --git a/packages/worker/yarn.lock b/packages/worker/yarn.lock index a5f5b6b95a..2f3313a1d9 100644 --- a/packages/worker/yarn.lock +++ b/packages/worker/yarn.lock @@ -293,10 +293,10 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@1.0.194": - version "1.0.194" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.0.194.tgz#08b2b1aec3c88efbc7cfb14145ce6f199475c538" - integrity sha512-BbnJFtAioJeD9tQfSwc2uftFK8SagREgSfUYv06dfnf/NsmkrONzZiTon1oA57S7ifcSiu+WZf87lNX0k8pwfQ== +"@budibase/backend-core@1.0.195": + version "1.0.195" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.0.195.tgz#ee40c690ae4a54febab8b140c9bbb7d04221f3b9" + integrity sha512-6diWgRV9t4DU3kXteJJAhCxyta9m1wvzN7vNbflhY4kYJeg7BC+7jcvc2r8zl6s1vVeSW4ic5/ueSLRaTDySuw== dependencies: "@techpass/passport-openidconnect" "^0.3.0" aws-sdk "^2.901.0" @@ -322,12 +322,12 @@ uuid "^8.3.2" zlib "^1.0.5" -"@budibase/pro@1.0.194": - version "1.0.194" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.0.194.tgz#fbf977b292b9a6dbf7b072b2e0379dcf4379943a" - integrity sha512-LSqVwlhKWwFNnC3acvLnCzbeBoze1Ma6GELE/b5ZxS65owsigu0KC6T/UuujEbU9xqbFJ3R6uV+4Fz4NUibLIw== +"@budibase/pro@1.0.195": + version "1.0.195" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.0.195.tgz#368652398d1da95f160fc0192b77144b11147ff5" + integrity sha512-zf1f1exHop4m6vda5hObUnTZa2PIBRnc4e0r9iqFbzvGBMfBLGUhGzu23JEwNYaS2xhWHj2FNv4/IVzIyLG4eA== dependencies: - "@budibase/backend-core" "1.0.194" + "@budibase/backend-core" "1.0.195" node-fetch "^2.6.1" "@cspotcode/source-map-consumer@0.8.0":