diff --git a/packages/server/package.json b/packages/server/package.json index 28be6de1a2..2c7b619a3a 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -77,6 +77,7 @@ "jimp": "0.16.1", "joi": "17.2.1", "jsonschema": "1.4.0", + "knex": "^0.95.6", "koa": "2.7.0", "koa-body": "4.2.0", "koa-compress": "4.0.1", diff --git a/packages/server/src/integrations/base/constants.js b/packages/server/src/integrations/base/constants.js new file mode 100644 index 0000000000..01cde75fd6 --- /dev/null +++ b/packages/server/src/integrations/base/constants.js @@ -0,0 +1,11 @@ +exports.Operation = { + CREATE: "CREATE", + READ: "READ", + UPDATE: "UPDATE", + DELETE: "DELETE", +} + +exports.SortDirection = { + ASCENDING: "ASCENDING", + DESCENDING: "DESCENDING", +} diff --git a/packages/server/src/integrations/base/sql.js b/packages/server/src/integrations/base/sql.js new file mode 100644 index 0000000000..33d4906bec --- /dev/null +++ b/packages/server/src/integrations/base/sql.js @@ -0,0 +1,124 @@ +const { Operation, SortDirection } = require("./constants") + +const BASE_LIMIT = 5000 + +function addFilters(query, filters) { + function iterate(structure, fn) { + for (let [key, value] of Object.entries(structure)) { + fn(key, value) + } + } + if (filters.string) { + iterate(filters.string, (key, value) => { + query = query.where(key, "like", `${value}%`) + }) + } + if (filters.range) { + iterate(filters.range, (key, value) => { + if (!value.high || !value.low) { + return + } + query = query.whereBetween(key, [value.low, value.high]) + }) + } + if (filters.equal) { + iterate(filters.equal, (key, value) => { + query = query.where({ [key]: value }) + }) + } + if (filters.notEqual) { + iterate(filters.notEqual, (key, value) => { + query = query.whereNot({ [key]: value }) + }) + } + if (filters.empty) { + iterate(filters.empty, key => { + query = query.whereNull(key) + }) + } + if (filters.notEmpty) { + iterate(filters.notEmpty, key => { + query = query.whereNotNull(key) + }) + } + return query +} + +function buildCreate(knex, json) { + const { endpoint, body } = json + let query = knex(endpoint.entityId) + return query.insert(body).toString() +} + +function buildRead(knex, json, limit) { + const { endpoint, resource, filters, sort, paginate } = json + let query = knex(endpoint.entityId) + // handle select + if (resource.fields && resource.fields.length > 0) { + query = query.select(resource.fields) + } else { + query = query.select("*") + } + // handle where + query = addFilters(query, filters) + // handle sorting + if (sort) { + for (let [key, value] of Object.entries(sort)) { + const direction = value === SortDirection.ASCENDING ? "asc" : "desc" + query = query.orderBy(key, direction) + } + } + // handle pagination + if (paginate.page && paginate.limit) { + const page = paginate.page <= 1 ? 0 : paginate.page - 1 + const offset = page * paginate.limit + query = query.offset(offset).limit(paginate.limit) + } else if (paginate.limit) { + query = query.limit(paginate.limit) + } else { + query.limit(limit) + } + return query.toString() +} + +function buildUpdate(knex, json) { + const { endpoint, body, filters } = json + let query = knex(endpoint.entityId) + query = addFilters(query, filters) + return query.update(body).toString() +} + +function buildDelete(knex, json) { + const { endpoint, filters } = json + let query = knex(endpoint.entityId) + query = addFilters(query, filters) + return query.delete().toString() +} + +class SqlQueryBuilder { + // pass through client to get flavour of SQL + constructor(client, limit = BASE_LIMIT) { + this._client = client + this._limit = limit + } + + buildQuery(json) { + const { endpoint } = json + const knex = require("knex")({ client: this._client }) + const operation = endpoint.operation + switch (operation) { + case Operation.CREATE: + return buildCreate(knex, json) + case Operation.READ: + return buildRead(knex, json, this._limit) + case Operation.UPDATE: + return buildUpdate(knex, json) + case Operation.DELETE: + return buildDelete(knex, json) + default: + throw `Operation ${operation} type is not supported by SQL query builder` + } + } +} + +module.exports = SqlQueryBuilder diff --git a/packages/server/src/integrations/tests/sql.spec.js b/packages/server/src/integrations/tests/sql.spec.js new file mode 100644 index 0000000000..98a7286c8d --- /dev/null +++ b/packages/server/src/integrations/tests/sql.spec.js @@ -0,0 +1,120 @@ +const Sql = require("../base/sql") + +const TABLE_NAME = "test" + +function endpoint(table, operation) { + return { + datasourceId: "Postgres", + operation: operation, + entityId: table || TABLE_NAME, + } +} + +function generateReadJson({ table, fields, filters, sort, paginate} = {}) { + return { + endpoint: endpoint(table || TABLE_NAME, "READ"), + resource: { + fields: fields || [], + }, + filters: filters || {}, + sort: sort || {}, + paginate: paginate || {}, + } +} + +function generateCreateJson(table = TABLE_NAME, body = {}) { + return { + endpoint: endpoint(table, "CREATE"), + body, + } +} + +function generateUpdateJson(table = TABLE_NAME, body = {}, filters = {}) { + return { + endpoint: endpoint(table, "UPDATE"), + filters, + body, + } +} + +function generateDeleteJson(table = TABLE_NAME, filters = {}) { + return { + endpoint: endpoint(table, "DELETE"), + filters, + } +} + +describe("SQL query builder", () => { + const limit = 500 + const client = "pg" + let sql + + beforeEach(() => { + sql = new Sql(client, limit) + }) + + it("should test a basic read", () => { + const query = sql.buildQuery(generateReadJson()) + expect(query).toEqual(`select * from "${TABLE_NAME}" limit ${limit}`) + }) + + it("should test a read with specific columns", () => { + const query = sql.buildQuery(generateReadJson({ + fields: ["name", "age"] + })) + expect(query).toEqual(`select "name", "age" from "${TABLE_NAME}" limit ${limit}`) + }) + + it("should test a where string starts with read", () => { + const query = sql.buildQuery(generateReadJson({ + filters: { + string: { + name: "John", + } + } + })) + expect(query).toEqual(`select * from "${TABLE_NAME}" where "name" like 'John%' limit ${limit}`) + }) + + it("should test a where range read", () => { + const query = sql.buildQuery(generateReadJson({ + filters: { + range: { + age: { + low: 2, + high: 10, + } + } + } + })) + expect(query).toEqual(`select * from "${TABLE_NAME}" where "age" between 2 and 10 limit ${limit}`) + }) + + it("should test an create statement", () => { + const query = sql.buildQuery(generateCreateJson(TABLE_NAME, { + name: "Michael", + age: 45, + })) + expect(query).toEqual(`insert into "${TABLE_NAME}" ("age", "name") values (45, 'Michael')`) + }) + + it("should test an update statement", () => { + const query = sql.buildQuery(generateUpdateJson(TABLE_NAME, { + name: "John" + }, { + equal: { + id: 1001, + } + })) + expect(query).toEqual(`update "${TABLE_NAME}" set "name" = 'John' where "id" = 1001`) + }) + + it("should test a delete statement", () => { + const query = sql.buildQuery(generateDeleteJson(TABLE_NAME, { + equal: { + id: 1001, + } + })) + expect(query).toEqual(`delete from "${TABLE_NAME}" where "id" = 1001`) + }) +}) \ No newline at end of file diff --git a/packages/server/yarn.lock b/packages/server/yarn.lock index 59c8ac127f..197fa14290 100644 --- a/packages/server/yarn.lock +++ b/packages/server/yarn.lock @@ -1057,10 +1057,10 @@ "@babel/helper-validator-identifier" "^7.14.0" to-fast-properties "^2.0.0" -"@budibase/auth@^0.9.19": - version "0.9.19" - resolved "https://registry.yarnpkg.com/@budibase/auth/-/auth-0.9.19.tgz#f939dabed8c5294d850b4bbac972659f1f65927d" - integrity sha512-9jOW+yntZ9296KqSX6BQyyxqqe6geqfdhPBurvFRfPe9hngiMXuOrdxIsYe1DFtJOAQdJE7leV0lpdDf15QznQ== +"@budibase/auth@^0.9.24": + version "0.9.24" + resolved "https://registry.yarnpkg.com/@budibase/auth/-/auth-0.9.24.tgz#7c8e02076025d97734a5815a7c17562a566c23c2" + integrity sha512-uYYw29mOtzeQ16uBaOmcT+OabGhXxeTcXFexayZDv4s9d6F3wpvxi/t7MMemCmUP0cSxuFd9DOiJj58iMLILDA== dependencies: aws-sdk "^2.901.0" bcryptjs "^2.4.3" @@ -1078,10 +1078,10 @@ uuid "^8.3.2" zlib "^1.0.5" -"@budibase/bbui@^0.9.19": - version "0.9.19" - resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-0.9.19.tgz#72f10b46ce59de0fe381d200c62bfafd94c6d1ef" - integrity sha512-TBVE9iS+BNYj/F6s0guGBAb3HLRABDoSRQ+HuM66890g6d7BcxkjYn5mK8v3TDNw9OzTP1MVsPTNF6doH1nsAA== +"@budibase/bbui@^0.9.24": + version "0.9.24" + resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-0.9.24.tgz#ee0c2b5cbbf478492b525ef365c09311b0a59a70" + integrity sha512-FaZo1zk2CN/MeCarAw9qaDMO3PDIBzGTV5cZ1wHg5KDAEoTx4xLkxDdCbJOLbA42zBPUks5Mru7C9qomQUc8yQ== dependencies: "@adobe/spectrum-css-workflow-icons" "^1.2.1" "@spectrum-css/actionbutton" "^1.0.1" @@ -1126,12 +1126,12 @@ svelte-flatpickr "^3.1.0" svelte-portal "^1.0.0" -"@budibase/client@^0.9.19": - version "0.9.19" - resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.9.19.tgz#1be110304cc431bbe958eea086e7dc30e1fe261f" - integrity sha512-5Zf5aEnI4PEg1sOlLtaIZNifQpF2zfmRvG6EIGtCcgF3a0JJd+8Pwnhyki/X9SxhOhJZIboyJAufTD9LqjfVcQ== +"@budibase/client@^0.9.24": + version "0.9.24" + resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.9.24.tgz#a6bcc641f961715c1701c4dcc03fe0fb7a8de9bd" + integrity sha512-L3MIpRU0ncsFh9uPk2BcoZ1D52Of1LZNJGWxPt8QdhBvtLuKqD6sM3rdKuUA0EsUOn7hgPIZl82SEolQQz9Dyw== dependencies: - "@budibase/string-templates" "^0.9.19" + "@budibase/string-templates" "^0.9.24" regexparam "^1.3.0" shortid "^2.2.15" svelte-spa-router "^3.0.5" @@ -1168,22 +1168,22 @@ to-gfm-code-block "^0.1.1" year "^0.2.1" -"@budibase/standard-components@^0.9.19": - version "0.9.19" - resolved "https://registry.yarnpkg.com/@budibase/standard-components/-/standard-components-0.9.19.tgz#0acceee62334144229069680b887e80f16d37280" - integrity sha512-geq48LKT5JFHkmfhTKtvvV+9Iu5zEziEJga5zGh8r29JZiVW0W8a3qx2fTGlw65m3yqFUgtlVD/hSGgWXlatsw== +"@budibase/standard-components@^0.9.24": + version "0.9.24" + resolved "https://registry.yarnpkg.com/@budibase/standard-components/-/standard-components-0.9.24.tgz#f14b81fea6a1b2f7d5212b523ec9097be505af89" + integrity sha512-TWdfi044EaT1NigZB02NmTARfamlN8anHjc87QZfNdbdPxD9tgLxcPe/sPaPd5ACBjVC6McL3sKJs2xCbB78Ig== dependencies: - "@budibase/bbui" "^0.9.19" + "@budibase/bbui" "^0.9.24" "@spectrum-css/page" "^3.0.1" "@spectrum-css/vars" "^3.0.1" apexcharts "^3.22.1" svelte-apexcharts "^1.0.2" svelte-flatpickr "^3.1.0" -"@budibase/string-templates@^0.9.19": - version "0.9.19" - resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-0.9.19.tgz#a579fda12167f00ef08fc9528710883214814e8d" - integrity sha512-CD6pzNYwjtIxNofnwtsQWkI9Ty/3GCP63ekYAAR+ox/pd0mNmcG7SZiSorb2ZeLCMujI4afHuDoTVPy28su33g== +"@budibase/string-templates@^0.9.24": + version "0.9.24" + resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-0.9.24.tgz#8feea5e1510b8e615b5db2c58b7907b64ebdd8bf" + integrity sha512-CeACQYbzsxrvjcrcbzEIRHbcJgy7siXz1ybxBxgErcKX/WhVpPKKAcviyW+5m9RnRWpD3OWOfHFZdzrVreVxjw== dependencies: "@budibase/handlebars-helpers" "^0.11.3" dayjs "^1.10.4" @@ -3630,6 +3630,11 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +colorette@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" + integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== + colorette@^1.2.1, colorette@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" @@ -3647,6 +3652,11 @@ commander@^2.5.0, commander@^2.8.1: resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== +commander@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + commoner@^0.10.1: version "0.10.8" resolved "https://registry.yarnpkg.com/commoner/-/commoner-0.10.8.tgz#34fc3672cd24393e8bb47e70caa0293811f4f2c5" @@ -3868,7 +3878,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3: dependencies: ms "2.0.0" -debug@4, debug@^4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: +debug@4, debug@4.3.1, debug@^4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== @@ -4426,6 +4436,11 @@ eslint@^6.8.0: text-table "^0.2.0" v8-compile-cache "^2.0.3" +esm@^3.2.25: + version "3.2.25" + resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10" + integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA== + esmangle-evaluator@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/esmangle-evaluator/-/esmangle-evaluator-1.0.1.tgz#620d866ef4861b3311f75766d52a8572bb3c6336" @@ -5111,6 +5126,11 @@ get-value@^2.0.3, get-value@^2.0.6: resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= +getopts@2.2.5: + version "2.2.5" + resolved "https://registry.yarnpkg.com/getopts/-/getopts-2.2.5.tgz#67a0fe471cacb9c687d817cab6450b96dde8313b" + integrity sha512-9jb7AW5p3in+IiJWhQiZmmwkpLaR/ccTWdWQCtZM66HJcHHLegowh4q4tSD7gouUyeNvFWRavfK9GXosQHDpFA== + getpass@^0.1.1: version "0.1.7" resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" @@ -5679,6 +5699,11 @@ inquirer@^7.0.0: strip-ansi "^6.0.0" through "^2.3.6" +interpret@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" + integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== + into-stream@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/into-stream/-/into-stream-3.1.0.tgz#96fb0a936c12babd6ff1752a17d05616abd094c6" @@ -6865,6 +6890,25 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== +knex@^0.95.6: + version "0.95.6" + resolved "https://registry.yarnpkg.com/knex/-/knex-0.95.6.tgz#5fc60ffc2935567bf122925526b1b06b8dbca785" + integrity sha512-noRcmkJl1MdicUbezrcr8OtVLcqQ/cfLIwgAx5EaxNxQOIJff88rBeyLywUScGhQNd/b78DIKKXZzLMrm6h/cw== + dependencies: + colorette "1.2.1" + commander "^7.1.0" + debug "4.3.1" + escalade "^3.1.1" + esm "^3.2.25" + getopts "2.2.5" + interpret "^2.2.0" + lodash "^4.17.21" + pg-connection-string "2.4.0" + rechoir "^0.7.0" + resolve-from "^5.0.0" + tarn "^3.0.1" + tildify "2.0.0" + koa-body@4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/koa-body/-/koa-body-4.2.0.tgz#37229208b820761aca5822d14c5fc55cee31b26f" @@ -8337,7 +8381,7 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= -pg-connection-string@^2.4.0: +pg-connection-string@2.4.0, pg-connection-string@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.4.0.tgz#c979922eb47832999a204da5dbe1ebf2341b6a10" integrity sha512-3iBXuv7XKvxeMrIgym7njT+HlZkwZqqGX4Bu9cci8xHZNT+Um1gWKqCsAzcC0d95rcKMU5WBg6YRUcHyV0HZKQ== @@ -9065,6 +9109,13 @@ recast@^0.11.17: private "~0.1.5" source-map "~0.5.0" +rechoir@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.7.0.tgz#32650fd52c21ab252aa5d65b19310441c7e03aca" + integrity sha512-ADsDEH2bvbjltXEP+hTIAmeFekTFK0V2BTxMkok6qILyAJEXV0AFfoWcAq4yfll5VdIMd/RVXq0lR+wQi5ZU3Q== + dependencies: + resolve "^1.9.0" + redis-commands@1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.7.0.tgz#15a6fea2d58281e27b1cd1acfb4b293e278c3a89" @@ -9304,7 +9355,7 @@ resolve@1.1.7: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@^1.10.0, resolve@^1.14.2: +resolve@^1.10.0, resolve@^1.14.2, resolve@^1.9.0: version "1.20.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== @@ -10205,6 +10256,11 @@ tarn@^1.1.5: resolved "https://registry.yarnpkg.com/tarn/-/tarn-1.1.5.tgz#7be88622e951738b9fa3fb77477309242cdddc2d" integrity sha512-PMtJ3HCLAZeedWjJPgGnCvcphbCOMbtZpjKgLq3qM5Qq9aQud+XHrL0WlrlgnTyS8U+jrjGbEXprFcQrxPy52g== +tarn@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/tarn/-/tarn-3.0.1.tgz#ebac2c6dbc6977d34d4526e0a7814200386a8aec" + integrity sha512-6usSlV9KyHsspvwu2duKH+FMUhqJnAh6J5J/4MITl8s94iSUQTLkJggdiewKv4RyARQccnigV48Z+khiuVZDJw== + tedious@^6.6.2: version "6.7.0" resolved "https://registry.yarnpkg.com/tedious/-/tedious-6.7.0.tgz#ad02365f16f9e0416b216e13d3f83c53addd42ca" @@ -10306,6 +10362,11 @@ through@^2.3.6, through@^2.3.8, through@~2.3.4: resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= +tildify@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/tildify/-/tildify-2.0.0.tgz#f205f3674d677ce698b7067a99e949ce03b4754a" + integrity sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw== + time-stamp@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3"