diff --git a/.changeset/warm-elephants-give.md b/.changeset/warm-elephants-give.md new file mode 100644 index 000000000..66a9e3417 --- /dev/null +++ b/.changeset/warm-elephants-give.md @@ -0,0 +1,5 @@ +--- +'@electric-sql/pglite': patch +--- + +Added pg_hashids extension. diff --git a/docs/extensions/extensions.data.ts b/docs/extensions/extensions.data.ts index de0fde1c5..d7fe4e035 100644 --- a/docs/extensions/extensions.data.ts +++ b/docs/extensions/extensions.data.ts @@ -545,6 +545,22 @@ const baseExtensions: Extension[] = [ importName: 'pg_ivm', size: 24865, }, + { + name: 'pg_hashids', + description: ` + Hashids is a small open-source library that generates short, unique, non-sequential + ids from numbers. It converts numbers like 347 into strings like “yr8”. You can also + decode those ids back. This is useful in bundling several parameters into one or simply + using them as short UIDs. + `, + shortDescription: + 'Short unique id generator for PostgreSQL, using hashids.', + docs: 'https://github.com/iCyberon/pg_hashids', + tags: ['postgres extension'], + importPath: '@electric-sql/pglite/pg_hashids', + importName: 'pg_hashids', + size: 4212, + }, ] const tags = [ diff --git a/docs/repl/allExtensions.ts b/docs/repl/allExtensions.ts index 1cfb1a674..3551ba37f 100644 --- a/docs/repl/allExtensions.ts +++ b/docs/repl/allExtensions.ts @@ -32,3 +32,4 @@ export { tsm_system_time } from '@electric-sql/pglite/contrib/tsm_system_time' export { unaccent } from '@electric-sql/pglite/contrib/unaccent' export { uuid_ossp } from '@electric-sql/pglite/contrib/uuid_ossp' export { vector } from '@electric-sql/pglite/vector' +export { pg_hashids } from '@electric-sql/pglite/pg_hashids' diff --git a/packages/pglite/package.json b/packages/pglite/package.json index 8d7b326bd..9a85b0e10 100644 --- a/packages/pglite/package.json +++ b/packages/pglite/package.json @@ -134,6 +134,16 @@ "types": "./dist/contrib/*.d.ts", "import": "./dist/contrib/*.js", "require": "./dist/contrib/*.cjs" + }, + "./pg_hashids": { + "import": { + "types": "./dist/pg_hashids/index.d.ts", + "default": "./dist/pg_hashids/index.js" + }, + "require": { + "types": "./dist/pg_hashids/index.d.cts", + "default": "./dist/pg_hashids/index.cjs" + } } }, "type": "module", diff --git a/packages/pglite/scripts/bundle-wasm.ts b/packages/pglite/scripts/bundle-wasm.ts index 7d45f9965..30465ea44 100644 --- a/packages/pglite/scripts/bundle-wasm.ts +++ b/packages/pglite/scripts/bundle-wasm.ts @@ -79,6 +79,7 @@ async function main() { `require("./postgres.cjs").default`, ['.cjs'], ) + await findAndReplaceInDir('./dist/pg_hashids', /\.\.\/release\//g, '', ['.js', '.cjs']) } await main() diff --git a/packages/pglite/src/pg_hashids/index.ts b/packages/pglite/src/pg_hashids/index.ts new file mode 100644 index 000000000..21b6742bc --- /dev/null +++ b/packages/pglite/src/pg_hashids/index.ts @@ -0,0 +1,17 @@ +import type { + Extension, + ExtensionSetupResult, + PGliteInterface, +} from '../interface' + +const setup = async (_pg: PGliteInterface, emscriptenOpts: any) => { + return { + emscriptenOpts, + bundlePath: new URL('../../release/pg_hashids.tar.gz', import.meta.url), + } satisfies ExtensionSetupResult +} + +export const pg_hashids = { + name: 'pg_hashids', + setup, +} satisfies Extension diff --git a/packages/pglite/tests/pg_hashids.test.ts b/packages/pglite/tests/pg_hashids.test.ts new file mode 100644 index 000000000..61224c857 --- /dev/null +++ b/packages/pglite/tests/pg_hashids.test.ts @@ -0,0 +1,193 @@ +import { describe, it, expect } from 'vitest' +import { testEsmCjsAndDTC } from './test-utils.ts' + +await testEsmCjsAndDTC(async (importType) => { + const { PGlite } = + importType === 'esm' + ? await import('../dist/index.js') + : ((await import( + '../dist/index.cjs' + )) as unknown as typeof import('../dist/index.js')) + + const { pg_hashids } = + importType === 'esm' + ? await import('../dist/pg_hashids/index.js') + : ((await import( + '../dist/pg_hashids/index.cjs' + )) as unknown as typeof import('../dist/pg_hashids/index.js')) + + describe(`pg_hashids`, () => { + it('can load extension', async () => { + const pg = new PGlite({ + extensions: { + pg_hashids, + }, + }) + + await pg.exec('CREATE EXTENSION IF NOT EXISTS pg_hashids;') + + const res = await pg.query<{ extname: string }>(` + SELECT extname + FROM pg_extension + WHERE extname = 'pg_hashids' + `) + + expect(res.rows).toHaveLength(1) + expect(res.rows[0].extname).toBe('pg_hashids') + }) + + it('should return a hash using the default alphabet and empty salt', async () => { + const pg = new PGlite({ + extensions: { + pg_hashids, + }, + }) + + await pg.exec('CREATE EXTENSION IF NOT EXISTS pg_hashids;') + + const res = await pg.exec(`SELECT id_encode(1001);`) + + expect(res[0].rows[0].id_encode).toEqual('jNl') + }) + + it('should return a hash using the default alphabet and supplied salt', async () => { + const pg = new PGlite({ + extensions: { + pg_hashids, + }, + }) + + await pg.exec('CREATE EXTENSION IF NOT EXISTS pg_hashids;') + + const res = await pg.exec(`SELECT id_encode(1234567, 'This is my salt');`) + + expect(res[0].rows[0].id_encode).toEqual('Pdzxp') + }) + + it('should return a hash using the default alphabet, salt and minimum hash length', async () => { + const pg = new PGlite({ + extensions: { + pg_hashids, + }, + }) + + await pg.exec('CREATE EXTENSION IF NOT EXISTS pg_hashids;') + + const res = await pg.exec( + `SELECT id_encode(1234567, 'This is my salt', 10);`, + ) + + expect(res[0].rows[0].id_encode).toEqual('PlRPdzxpR7') + }) + + it('should return a hash using the supplied alphabet, salt and minimum hash length', async () => { + const pg = new PGlite({ + extensions: { + pg_hashids, + }, + }) + + await pg.exec('CREATE EXTENSION IF NOT EXISTS pg_hashids;') + + const res = await pg.exec( + `SELECT id_encode(1234567, 'This is my salt', 10, 'abcdefghijABCDxFGHIJ1234567890');`, + ) + + expect(res[0].rows[0].id_encode).toEqual('3GJ956J9B9') + }) + + it('should decode previously generated hash', async () => { + const pg = new PGlite({ + extensions: { + pg_hashids, + }, + }) + + await pg.exec('CREATE EXTENSION IF NOT EXISTS pg_hashids;') + + const res = await pg.exec( + `SELECT id_decode('PlRPdzxpR7', 'This is my salt', 10);`, + ) + + expect(res[0].rows[0].id_decode).toEqual([1234567]) + }) + + it('should decode previously generated hash using the supplied alphabet', async () => { + const pg = new PGlite({ + extensions: { + pg_hashids, + }, + }) + + await pg.exec('CREATE EXTENSION IF NOT EXISTS pg_hashids;') + + const res = await pg.exec( + `SELECT id_decode('3GJ956J9B9', 'This is my salt', 10, 'abcdefghijABCDxFGHIJ1234567890');`, + ) + + expect(res[0].rows[0].id_decode).toEqual([1234567]) + }) + + it('should decode previously generated hash into a single integer', async () => { + const pg = new PGlite({ + extensions: { + pg_hashids, + }, + }) + + await pg.exec('CREATE EXTENSION IF NOT EXISTS pg_hashids;') + + const res = await pg.exec(`SELECT id_decode_once('jNl');`) + + expect(res[0].rows[0].id_decode_once).toEqual(1001) + }) + + it('should decode previously generated hash into a single integer using the supplied salt', async () => { + const pg = new PGlite({ + extensions: { + pg_hashids, + }, + }) + + await pg.exec('CREATE EXTENSION IF NOT EXISTS pg_hashids;') + + const res = await pg.exec( + `SELECT id_decode_once('Pdzxp', 'This is my salt');`, + ) + + expect(res[0].rows[0].id_decode_once).toEqual(1234567) + }) + + it('should decode previously generated hash into a single integer using the supplied salt and minimum hash length', async () => { + const pg = new PGlite({ + extensions: { + pg_hashids, + }, + }) + + await pg.exec('CREATE EXTENSION IF NOT EXISTS pg_hashids;') + + const res = await pg.exec( + `SELECT id_decode_once('PlRPdzxpR7', 'This is my salt', 10);`, + ) + + expect(res[0].rows[0].id_decode_once).toEqual(1234567) + }) + + it('should decode previously generated hash into a single integer using the supplied alphabet', async () => { + const pg = new PGlite({ + extensions: { + pg_hashids, + }, + }) + + await pg.exec('CREATE EXTENSION IF NOT EXISTS pg_hashids;') + + const res = await pg.exec( + `SELECT id_decode_once('3GJ956J9B9', 'This is my salt', 10, 'abcdefghijABCDxFGHIJ1234567890');`, + ) + + expect(res[0].rows[0].id_decode_once).toEqual(1234567) + }) + }) +}) diff --git a/packages/pglite/tsup.config.ts b/packages/pglite/tsup.config.ts index e4cadffa8..a73508d8b 100644 --- a/packages/pglite/tsup.config.ts +++ b/packages/pglite/tsup.config.ts @@ -28,6 +28,7 @@ const entryPoints = [ 'src/pgtap/index.ts', 'src/pg_uuidv7/index.ts', 'src/worker/index.ts', + 'src/pg_hashids/index.ts', ] const contribDir = path.join(root, 'src', 'contrib') diff --git a/postgres-pglite b/postgres-pglite index 1195d5388..3e6196922 160000 --- a/postgres-pglite +++ b/postgres-pglite @@ -1 +1 @@ -Subproject commit 1195d5388bd5529e0013c45fa816cfcd953d84e0 +Subproject commit 3e61969226dc2bc0010b9617e755cba62a9a1540