diff --git a/README.md b/README.md index a13246e8..7a34264f 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ To create a new tool, there is a script that generate the boilerplate of the new pnpm run script:create:tool my-tool-name ``` -It will create a directory in `src/tools` with the correct files, and a the import in `src/tools/index.ts`. You will just need to add the imported tool in the proper category and develop the tool. +It will create a directory in `src/tools` with the correct files, and add the import in `src/tools/index.ts`. You will just need to add the imported tool in the proper category and develop the tool. ## Contributors diff --git a/components.d.ts b/components.d.ts index 3e65c3cc..f1c5c771 100644 --- a/components.d.ts +++ b/components.d.ts @@ -90,17 +90,28 @@ declare module '@vue/runtime-core' { HttpStatusCodes: typeof import('./src/tools/http-status-codes/http-status-codes.vue')['default'] IbanValidatorAndParser: typeof import('./src/tools/iban-validator-and-parser/iban-validator-and-parser.vue')['default'] 'IconMdi:brushVariant': typeof import('~icons/mdi/brush-variant')['default'] + 'IconMdi:contentCopy': typeof import('~icons/mdi/content-copy')['default'] 'IconMdi:kettleSteamOutline': typeof import('~icons/mdi/kettle-steam-outline')['default'] + IconMdiArrowDown: typeof import('~icons/mdi/arrow-down')['default'] + IconMdiArrowRightBottom: typeof import('~icons/mdi/arrow-right-bottom')['default'] + IconMdiCamera: typeof import('~icons/mdi/camera')['default'] IconMdiChevronDown: typeof import('~icons/mdi/chevron-down')['default'] IconMdiChevronRight: typeof import('~icons/mdi/chevron-right')['default'] IconMdiClose: typeof import('~icons/mdi/close')['default'] IconMdiContentCopy: typeof import('~icons/mdi/content-copy')['default'] + IconMdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default'] + IconMdiDownload: typeof import('~icons/mdi/download')['default'] IconMdiEye: typeof import('~icons/mdi/eye')['default'] IconMdiEyeOff: typeof import('~icons/mdi/eye-off')['default'] IconMdiHeart: typeof import('~icons/mdi/heart')['default'] + IconMdiPause: typeof import('~icons/mdi/pause')['default'] + IconMdiPlay: typeof import('~icons/mdi/play')['default'] + IconMdiRecord: typeof import('~icons/mdi/record')['default'] + IconMdiRefresh: typeof import('~icons/mdi/refresh')['default'] IconMdiSearch: typeof import('~icons/mdi/search')['default'] IconMdiTranslate: typeof import('~icons/mdi/translate')['default'] IconMdiTriangleDown: typeof import('~icons/mdi/triangle-down')['default'] + IconMdiVideo: typeof import('~icons/mdi/video')['default'] InputCopyable: typeof import('./src/components/InputCopyable.vue')['default'] IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default'] Ipv4AddressConverter: typeof import('./src/tools/ipv4-address-converter/ipv4-address-converter.vue')['default'] @@ -129,21 +140,44 @@ declare module '@vue/runtime-core' { MenuLayout: typeof import('./src/components/MenuLayout.vue')['default'] MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default'] MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default'] + NAlert: typeof import('naive-ui')['NAlert'] NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default'] + NButton: typeof import('naive-ui')['NButton'] NCheckbox: typeof import('naive-ui')['NCheckbox'] + NCode: typeof import('naive-ui')['NCode'] NCollapseTransition: typeof import('naive-ui')['NCollapseTransition'] + NColorPicker: typeof import('naive-ui')['NColorPicker'] NConfigProvider: typeof import('naive-ui')['NConfigProvider'] + NDatePicker: typeof import('naive-ui')['NDatePicker'] NDivider: typeof import('naive-ui')['NDivider'] + NDynamicInput: typeof import('naive-ui')['NDynamicInput'] NEllipsis: typeof import('naive-ui')['NEllipsis'] + NForm: typeof import('naive-ui')['NForm'] + NFormItem: typeof import('naive-ui')['NFormItem'] + NGi: typeof import('naive-ui')['NGi'] + NGrid: typeof import('naive-ui')['NGrid'] NH1: typeof import('naive-ui')['NH1'] + NH2: typeof import('naive-ui')['NH2'] NH3: typeof import('naive-ui')['NH3'] NIcon: typeof import('naive-ui')['NIcon'] + NImage: typeof import('naive-ui')['NImage'] + NInputGroup: typeof import('naive-ui')['NInputGroup'] + NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel'] + NInputNumber: typeof import('naive-ui')['NInputNumber'] NLayout: typeof import('naive-ui')['NLayout'] NLayoutSider: typeof import('naive-ui')['NLayoutSider'] NMenu: typeof import('naive-ui')['NMenu'] + NProgress: typeof import('naive-ui')['NProgress'] + NScrollbar: typeof import('naive-ui')['NScrollbar'] + NSlider: typeof import('naive-ui')['NSlider'] NSpace: typeof import('naive-ui')['NSpace'] + NSpin: typeof import('naive-ui')['NSpin'] + NStatistic: typeof import('naive-ui')['NStatistic'] + NSwitch: typeof import('naive-ui')['NSwitch'] NTable: typeof import('naive-ui')['NTable'] + NTag: typeof import('naive-ui')['NTag'] NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default'] + ObjectId: typeof import('./src/tools/object-id/object-id.vue')['default'] OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default'] PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default'] PdfSignatureChecker: typeof import('./src/tools/pdf-signature-checker/pdf-signature-checker.vue')['default'] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5d113893..3d07f847 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2434,8 +2434,8 @@ packages: '@vueuse/shared@10.3.0': resolution: {integrity: sha512-kGqCTEuFPMK4+fNWy6dUOiYmxGcUbtznMwBZLC1PubidF4VZY05B+Oht7Jh7/6x4VOWGpvu3R37WHi81cKpiqg==} - '@vueuse/shared@12.0.0': - resolution: {integrity: sha512-3i6qtcq2PIio5i/vVYidkkcgvmTjCqrf26u+Fd4LhnbBmIT6FN8y6q/GJERp8lfcB9zVEfjdV0Br0443qZuJpw==} + '@vueuse/shared@12.4.0': + resolution: {integrity: sha512-9yLgbHVIF12OSCojnjTIoZL1+UA10+O4E1aD6Hpfo/DKVm5o3SZIwz6CupqGy3+IcKI8d6Jnl26EQj/YucnW0Q==} '@zhead/schema@1.0.0-beta.13': resolution: {integrity: sha512-P1A1vRGFBhITco8Iw4/hvnDYoE/SoVrd71dW1pBFdXJb3vP+pBtoOuhbEKy0ROJGOyzQuqvFibcwzyLlWMqNiQ==} @@ -4868,10 +4868,6 @@ packages: resolution: {integrity: sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==} hasBin: true - resolve@1.22.8: - resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} - hasBin: true - resolve@1.22.9: resolution: {integrity: sha512-QxrmX1DzraFIi9PxdG5VkRfRwIgjwyud+z/iBwfRRrVmHc+P9Q7u2lSSpQ6bjr2gy5lrqIiU9vb6iAeGf2400A==} hasBin: true @@ -6117,7 +6113,7 @@ snapshots: '@babel/traverse': 7.23.2 '@babel/types': 7.23.0 convert-source-map: 2.0.0 - debug: 4.3.4 + debug: 4.4.0 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -7030,7 +7026,7 @@ snapshots: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.23.0 '@babel/types': 7.23.0 - debug: 4.3.4 + debug: 4.4.0 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -7401,7 +7397,7 @@ snapshots: '@linaria/logger@4.0.0': dependencies: - debug: 4.3.4 + debug: 4.4.0 picocolors: 1.0.0 transitivePeerDependencies: - supports-color @@ -7940,10 +7936,10 @@ snapshots: dependencies: '@typescript-eslint/types': 5.60.0 '@typescript-eslint/visitor-keys': 5.60.0 - debug: 4.3.4 + debug: 4.4.0 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.5.4 + semver: 7.6.3 tsutils: 3.21.0(typescript@5.2.2) optionalDependencies: typescript: 5.2.2 @@ -7968,10 +7964,10 @@ snapshots: dependencies: '@typescript-eslint/types': 6.9.1 '@typescript-eslint/visitor-keys': 6.9.1 - debug: 4.3.4 + debug: 4.4.0 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.5.4 + semver: 7.6.3 ts-api-utils: 1.0.1(typescript@5.2.2) optionalDependencies: typescript: 5.2.2 @@ -7988,7 +7984,7 @@ snapshots: '@typescript-eslint/typescript-estree': 5.60.0(typescript@5.2.2) eslint: 8.47.0 eslint-scope: 5.1.1 - semver: 7.5.4 + semver: 7.6.3 transitivePeerDependencies: - supports-color - typescript @@ -8053,7 +8049,7 @@ snapshots: dependencies: '@unhead/dom': 0.5.1 '@unhead/schema': 0.5.1 - '@vueuse/shared': 12.0.0(typescript@5.2.2) + '@vueuse/shared': 12.4.0(typescript@5.2.2) unhead: 0.5.1 vue: 3.3.4 transitivePeerDependencies: @@ -8580,7 +8576,7 @@ snapshots: - '@vue/composition-api' - vue - '@vueuse/shared@12.0.0(typescript@5.2.2)': + '@vueuse/shared@12.4.0(typescript@5.2.2)': dependencies: vue: 3.5.13(typescript@5.2.2) transitivePeerDependencies: @@ -10753,7 +10749,7 @@ snapshots: normalize-package-data@2.5.0: dependencies: hosted-git-info: 2.8.9 - resolve: 1.22.8 + resolve: 1.22.9 semver: 5.7.2 validate-npm-package-license: 3.0.4 @@ -11271,12 +11267,6 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - resolve@1.22.8: - dependencies: - is-core-module: 2.13.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - resolve@1.22.9: dependencies: is-core-module: 2.16.0 diff --git a/src/tools/date-time-converter/date-time-converter.models.test.ts b/src/tools/date-time-converter/date-time-converter.models.test.ts index c2c7bee9..e12d8b97 100644 --- a/src/tools/date-time-converter/date-time-converter.models.test.ts +++ b/src/tools/date-time-converter/date-time-converter.models.test.ts @@ -5,7 +5,6 @@ import { isExcelFormat, isISO8601DateTimeString, isISO9075DateString, - isMongoObjectId, isRFC3339DateString, isRFC7231DateString, isTimestamp, @@ -129,20 +128,6 @@ describe('date-time-converter models', () => { }); }); - describe('isMongoObjectId', () => { - test('should return true for valid Mongo ObjectIds', () => { - expect(isMongoObjectId('507f1f77bcf86cd799439011')).toBe(true); - expect(isMongoObjectId('507f1f77bcf86cd799439012')).toBe(true); - }); - - test('should return false for invalid Mongo ObjectIds', () => { - expect(isMongoObjectId('507f1f77bcf86cd79943901')).toBe(false); - expect(isMongoObjectId('507f1f77bcf86cd79943901z')).toBe(false); - expect(isMongoObjectId('foo')).toBe(false); - expect(isMongoObjectId('')).toBe(false); - }); - }); - describe('isExcelFormat', () => { test('an Excel format string is a floating number that can be negative', () => { expect(isExcelFormat('0')).toBe(true); diff --git a/src/tools/date-time-converter/date-time-converter.models.ts b/src/tools/date-time-converter/date-time-converter.models.ts index f5eedbfa..1af863f6 100644 --- a/src/tools/date-time-converter/date-time-converter.models.ts +++ b/src/tools/date-time-converter/date-time-converter.models.ts @@ -8,7 +8,6 @@ export { isUnixTimestamp, isTimestamp, isUTCDateString, - isMongoObjectId, dateToExcelFormat, excelFormatToDate, isExcelFormat, @@ -36,7 +35,6 @@ const isRFC3339DateString = createRegexMatcher(RFC3339_REGEX); const isRFC7231DateString = createRegexMatcher(RFC7231_REGEX); const isUnixTimestamp = createRegexMatcher(/^[0-9]{1,10}$/); const isTimestamp = createRegexMatcher(/^[0-9]{1,13}$/); -const isMongoObjectId = createRegexMatcher(/^[0-9a-fA-F]{24}$/); const isExcelFormat = createRegexMatcher(EXCEL_FORMAT_REGEX); diff --git a/src/tools/date-time-converter/date-time-converter.vue b/src/tools/date-time-converter/date-time-converter.vue index 5636ed46..f37a9afa 100644 --- a/src/tools/date-time-converter/date-time-converter.vue +++ b/src/tools/date-time-converter/date-time-converter.vue @@ -19,7 +19,6 @@ import { isExcelFormat, isISO8601DateTimeString, isISO9075DateString, - isMongoObjectId, isRFC3339DateString, isRFC7231DateString, isTimestamp, @@ -28,6 +27,7 @@ import { } from './date-time-converter.models'; import { withDefaultOnError } from '@/utils/defaults'; import { useValidation } from '@/composable/validation'; +import { isValidObjectId, objectIdFromDate, objectIdToDate } from '@/utils/objectId'; const inputDate = ref(''); @@ -84,9 +84,9 @@ const formats: DateFormat[] = [ }, { name: 'Mongo ObjectID', - fromDate: date => `${Math.floor(date.getTime() / 1000).toString(16)}0000000000000000`, - toDate: objectId => new Date(Number.parseInt(objectId.substring(0, 8), 16) * 1000), - formatMatcher: date => isMongoObjectId(date), + fromDate: date => objectIdFromDate(date.getTime(), true), + toDate: objectIdToDate, + formatMatcher: date => isValidObjectId(date), }, { name: 'Excel date/time', diff --git a/src/tools/index.ts b/src/tools/index.ts index 388cfaf4..ee71bf3f 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -1,6 +1,7 @@ import { tool as base64FileConverter } from './base64-file-converter'; import { tool as base64StringConverter } from './base64-string-converter'; import { tool as basicAuthGenerator } from './basic-auth-generator'; +import { tool as objectId } from './object-id'; import { tool as emailNormalizer } from './email-normalizer'; import { tool as asciiTextDrawer } from './ascii-text-drawer'; @@ -153,6 +154,7 @@ export const toolsByCategory: ToolCategory[] = [ jsonMinify, jsonToCsv, sqlPrettify, + objectId, chmodCalculator, dockerRunToDockerComposeConverter, xmlFormatter, diff --git a/src/tools/object-id/index.ts b/src/tools/object-id/index.ts new file mode 100644 index 00000000..f06217f9 --- /dev/null +++ b/src/tools/object-id/index.ts @@ -0,0 +1,12 @@ +import { Leaf } from '@vicons/tabler'; +import { defineTool } from '../tool'; + +export const tool = defineTool({ + name: 'ObjectId', + path: '/object-id', + description: 'MongoDB ObjectId parser and generator', + keywords: ['mongo', 'mongodb', 'object-id', 'object', 'id'], + component: () => import('./object-id.vue'), + icon: Leaf, + createdAt: new Date('2025-01-21'), +}); diff --git a/src/tools/object-id/object-id.vue b/src/tools/object-id/object-id.vue new file mode 100644 index 00000000..0dc08c42 --- /dev/null +++ b/src/tools/object-id/object-id.vue @@ -0,0 +1,85 @@ + + + diff --git a/src/utils/objectId.test.ts b/src/utils/objectId.test.ts new file mode 100644 index 00000000..0861f122 --- /dev/null +++ b/src/utils/objectId.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, test } from 'vitest'; +import { isValidObjectId, objectIdFromDate, objectIdToDate } from '@/utils/objectId'; + +describe('ObejctId', () => { + describe('isValidObjectId', () => { + test('should return true for valid Mongo ObjectIds', () => { + expect(isValidObjectId('507f1f77bcf86cd799439011')).toBe(true); + expect(isValidObjectId('507f1f77bcf86cd799439012')).toBe(true); + }); + + test('should return false for invalid Mongo ObjectIds', () => { + expect(isValidObjectId('507f1f77bcf86cd79943901')).toBe(false); + expect(isValidObjectId('507f1f77bcf86cd79943901z')).toBe(false); + expect(isValidObjectId('foo')).toBe(false); + expect(isValidObjectId('')).toBe(false); + }); + }); + + describe('objectIdToDate', () => { + test('should return Date from Mongo ObjectIds', () => { + expect(objectIdToDate('507f1f77bcf86cd799439011')).toStrictEqual(new Date('2012-10-17T23:13:27.000+0200')); + expect(objectIdToDate('678fd477d9bc2e855fdedfb0')).toStrictEqual(new Date('2025-01-21T18:08:07.000+0100')); + }); + }); + + describe('objectIdFromDate', () => { + test('should return Mongo ObjectId from Date', () => { + let objectId = objectIdFromDate(new Date('2012-10-17T23:13:27.000+0200').getTime()); + expect(isValidObjectId(objectId)).toBe(true); + expect(objectId).toMatch(/^507f1f77/); + + objectId = objectIdFromDate(new Date('2025-01-21T18:08:07.000+0100').getTime()); + expect(isValidObjectId(objectId)).toBe(true); + expect(objectId).toMatch(/^678fd477/); + }); + + test('should return Mongo ObjectId from Date, but only generate the date part', () => { + let objectId = objectIdFromDate(new Date('2012-10-17T23:13:27.000+0200').getTime(), true); + expect(isValidObjectId(objectId)).toBe(true); + expect(objectId).toMatch(/^507f1f770000000000000000$/); + + objectId = objectIdFromDate(new Date('2025-01-21T18:08:07.000+0100').getTime(), true); + expect(isValidObjectId(objectId)).toBe(true); + expect(objectId).toMatch(/^678fd4770000000000000000$/); + }); + }); +}); diff --git a/src/utils/objectId.ts b/src/utils/objectId.ts new file mode 100644 index 00000000..1ada9bfb --- /dev/null +++ b/src/utils/objectId.ts @@ -0,0 +1,24 @@ +import _ from 'lodash'; + +export function isValidObjectId(objectId?: string): boolean { + return !_.isNil(objectId) && /^[0-9a-fA-F]{24}$/.test(objectId); +} + +export function objectIdToDate(objectId: string): Date { + return new Date(Number.parseInt(objectId.substring(0, 8), 16) * 1000); +} + +export function objectIdFromDate(milliseconds: number, onlyDate: boolean = false): string { + const suffixReplacer = () => { + if (onlyDate) { + return '0'; + } + + return (_.random(0, 1, true) * 16 | 0).toString(16); + }; + + const timestamp = (milliseconds / 1000 | 0).toString(16); + const suffix = 'xxxxxxxxxxxxxxxx'.replace(/x/g, suffixReplacer).toLowerCase(); + + return `${timestamp}${suffix}`; +}