diff --git a/.github/workflows/data-table-integration.yaml b/.github/workflows/data-table-integration.yaml index 901d0db164d..3000a15fd2f 100644 --- a/.github/workflows/data-table-integration.yaml +++ b/.github/workflows/data-table-integration.yaml @@ -7,6 +7,7 @@ on: paths: - '.github/workflows/data-table-integration.yaml' - 'packages/data-table/**' + - 'packages/data-table-mssql/**' - 'packages/data-table-postgres/**' - 'packages/data-table-mysql/**' - 'packages/data-table-sqlite/**' @@ -16,6 +17,7 @@ on: paths: - '.github/workflows/data-table-integration.yaml' - 'packages/data-table/**' + - 'packages/data-table-mssql/**' - 'packages/data-table-postgres/**' - 'packages/data-table-mysql/**' - 'packages/data-table-sqlite/**' @@ -54,10 +56,53 @@ jobs: pnpm --filter @remix-run/data-table-mysql run typecheck pnpm --filter @remix-run/data-table-mysql run test pnpm --filter @remix-run/data-table-mysql run build + pnpm --filter @remix-run/data-table-mssql run typecheck + pnpm --filter @remix-run/data-table-mssql run test + pnpm --filter @remix-run/data-table-mssql run build pnpm --filter @remix-run/data-table-sqlite run typecheck pnpm --filter @remix-run/data-table-sqlite run test pnpm --filter @remix-run/data-table-sqlite run build + mssql-integration: + name: MSSQL Integration + runs-on: ubuntu-latest + + services: + mssql: + image: mcr.microsoft.com/mssql/server:2022-latest + env: + ACCEPT_EULA: 'Y' + SA_PASSWORD: 'YourStrong!Passw0rd' + ports: + - 1433:1433 + options: >- + --health-cmd="/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P 'YourStrong!Passw0rd' -Q 'SELECT 1' -C" + --health-interval=10s + --health-timeout=5s + --health-retries=20 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version-file: 'package.json' + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run mssql integration tests + env: + DATA_TABLE_INTEGRATION: '1' + DATA_TABLE_MSSQL_URL: Server=127.0.0.1,1433;Database=master;User Id=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=true;Encrypt=false + run: pnpm --filter @remix-run/data-table-mssql run test + postgres-integration: name: Postgres Integration runs-on: ubuntu-latest diff --git a/packages/data-table-mssql/.changes/README.md b/packages/data-table-mssql/.changes/README.md new file mode 100644 index 00000000000..5a5a0748606 --- /dev/null +++ b/packages/data-table-mssql/.changes/README.md @@ -0,0 +1,3 @@ +# Changes Directory + +See the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files. diff --git a/packages/data-table-mssql/.changes/minor.initial-release.md b/packages/data-table-mssql/.changes/minor.initial-release.md new file mode 100644 index 00000000000..d270b5d2724 --- /dev/null +++ b/packages/data-table-mssql/.changes/minor.initial-release.md @@ -0,0 +1 @@ +Initial release of `@remix-run/data-table-mssql`. diff --git a/packages/data-table-mssql/CHANGELOG.md b/packages/data-table-mssql/CHANGELOG.md new file mode 100644 index 00000000000..408d9daf655 --- /dev/null +++ b/packages/data-table-mssql/CHANGELOG.md @@ -0,0 +1,9 @@ +# `data-table-mssql` CHANGELOG + +This is the changelog for [`data-table-mssql`](https://github.com/remix-run/remix/tree/main/packages/data-table-mssql). It follows [semantic versioning](https://semver.org/). + +## v0.1.0 + +### Minor Changes + +- Initial release. diff --git a/packages/data-table-mssql/LICENSE b/packages/data-table-mssql/LICENSE new file mode 100644 index 00000000000..fbaa0226aeb --- /dev/null +++ b/packages/data-table-mssql/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Shopify Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/data-table-mssql/README.md b/packages/data-table-mssql/README.md new file mode 100644 index 00000000000..1c86058aa55 --- /dev/null +++ b/packages/data-table-mssql/README.md @@ -0,0 +1,97 @@ +# data-table-mssql + +MSSQL adapter for [`remix/data-table`](https://github.com/remix-run/remix/tree/main/packages/data-table). +Use this package when you want `data-table` APIs backed by `mssql`. + +## Features + +- **Native `mssql` Integration**: Works with `ConnectionPool` +- **Full `data-table` API Support**: Queries, relations, writes, and transactions +- **MSSQL Capabilities Enabled By Default**: + - `returning: false` + - `savepoints: true` + - `upsert: true` + +## Installation + +```sh +npm i remix mssql +``` + +## Usage + +```ts +import sql from 'mssql' +import { createDatabase } from 'remix/data-table' +import { createMssqlDatabaseAdapter } from 'remix/data-table-mssql' + +let pool = await sql.connect(process.env.DATABASE_URL) + +let db = createDatabase(createMssqlDatabaseAdapter(pool)) +``` + +Use `db.query(...)`, relation loading, and transactions from `remix/data-table`. + +## Adapter Capabilities + +`data-table-mssql` reports this capability set by default: + +- `returning: false` +- `savepoints: true` +- `upsert: true` + +### `returning` On MSSQL + +MSSQL does not support the `RETURNING` clause used by Postgres and SQLite. +When `returning` is `false` (the default), operations like +`db.create(table, values, { returnRow: true })` issue a follow-up `SELECT` to +fetch the created row. + +MSSQL _does_ support the `OUTPUT` clause which can serve a similar role. If you +enable the capability override the adapter will use `OUTPUT inserted.*` / +`OUTPUT deleted.*` instead of a follow-up query: + +```ts +let adapter = createMssqlDatabaseAdapter(pool, { + capabilities: { returning: true }, +}) +``` + +## Advanced Usage + +### Transaction Options + +Transaction options are passed through to the adapter as hints. + +```ts +await db.transaction(async (txDb) => txDb.exec('select 1'), { + isolationLevel: 'serializable', + readOnly: false, +}) +``` + +### Capability Overrides For Testing + +You can override capabilities to verify fallback paths in tests. + +```ts +import { createMssqlDatabaseAdapter } from 'remix/data-table-mssql' + +let adapter = createMssqlDatabaseAdapter(pool, { + capabilities: { + returning: true, + }, +}) +``` + +## Related Packages + +- [`data-table`](https://github.com/remix-run/remix/tree/main/packages/data-table) - Core query/relations API +- [`data-schema`](https://github.com/remix-run/remix/tree/main/packages/data-schema) - Schema definitions and validation +- [`data-table-postgres`](https://github.com/remix-run/remix/tree/main/packages/data-table-postgres) - Postgres adapter +- [`data-table-mysql`](https://github.com/remix-run/remix/tree/main/packages/data-table-mysql) - MySQL adapter +- [`data-table-sqlite`](https://github.com/remix-run/remix/tree/main/packages/data-table-sqlite) - SQLite adapter + +## License + +See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) diff --git a/packages/data-table-mssql/package.json b/packages/data-table-mssql/package.json new file mode 100644 index 00000000000..77fe9fe131d --- /dev/null +++ b/packages/data-table-mssql/package.json @@ -0,0 +1,68 @@ +{ + "name": "@remix-run/data-table-mssql", + "version": "0.0.0", + "description": "MSSQL adapter for @remix-run/data-table", + "author": "Michael Jackson ", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/remix-run/remix.git", + "directory": "packages/data-table-mssql" + }, + "homepage": "https://github.com/remix-run/remix/tree/main/packages/data-table-mssql#readme", + "files": [ + "LICENSE", + "README.md", + "dist", + "src", + "!src/**/*.test.ts" + ], + "type": "module", + "sideEffects": false, + "exports": { + ".": "./src/index.ts", + "./package.json": "./package.json" + }, + "publishConfig": { + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./package.json": "./package.json" + } + }, + "devDependencies": { + "@remix-run/data-schema": "workspace:*", + "@remix-run/data-table": "workspace:*", + "@types/mssql": "^9.1.8", + "@types/node": "catalog:", + "@typescript/native-preview": "catalog:", + "mssql": "^12.2.0" + }, + "dependencies": { + "@remix-run/data-table": "workspace:^" + }, + "peerDependencies": { + "mssql": "^12.2.0" + }, + "peerDependenciesMeta": { + "mssql": { + "optional": true + } + }, + "scripts": { + "build": "tsgo -p tsconfig.build.json", + "clean": "git clean -fdX", + "prepublishOnly": "pnpm run build", + "test": "node --disable-warning=ExperimentalWarning --test", + "typecheck": "tsgo --noEmit" + }, + "keywords": [ + "remix", + "orm", + "mssql", + "database", + "sql" + ] +} diff --git a/packages/data-table-mssql/src/index.ts b/packages/data-table-mssql/src/index.ts new file mode 100644 index 00000000000..fc103558776 --- /dev/null +++ b/packages/data-table-mssql/src/index.ts @@ -0,0 +1,8 @@ +export type { + MssqlDatabaseAdapterOptions, + MssqlDatabasePool, + MssqlDatabaseRequest, + MssqlDatabaseTransaction, + MssqlQueryResult, +} from './lib/adapter.ts' +export { createMssqlDatabaseAdapter, MssqlDatabaseAdapter } from './lib/adapter.ts' diff --git a/packages/data-table-mssql/src/lib/adapter.integration.test.ts b/packages/data-table-mssql/src/lib/adapter.integration.test.ts new file mode 100644 index 00000000000..f2b3f7ef72e --- /dev/null +++ b/packages/data-table-mssql/src/lib/adapter.integration.test.ts @@ -0,0 +1,218 @@ +import { after, before, beforeEach, describe, it } from 'node:test' +import assert from 'node:assert/strict' +import { nullable, number, string } from '@remix-run/data-schema' +import { createDatabase, createTable } from '@remix-run/data-table' +import sql, { type ConnectionPool } from 'mssql' + +import { runAdapterIntegrationContract } from '../../../data-table/test/adapter-integration-contract.ts' + +import { createMssqlDatabaseAdapter } from './adapter.ts' + +let integrationEnabled = + process.env.DATA_TABLE_INTEGRATION === '1' && typeof process.env.DATA_TABLE_MSSQL_URL === 'string' + +describe('mssql adapter integration', () => { + let pool: ConnectionPool + + before(async () => { + if (!integrationEnabled) { + return + } + + pool = await sql.connect(process.env.DATA_TABLE_MSSQL_URL as string) + + await pool.request().query('if object_id(\'tasks\', \'U\') is not null drop table [tasks]') + await pool.request().query('if object_id(\'projects\', \'U\') is not null drop table [projects]') + await pool.request().query('if object_id(\'accounts\', \'U\') is not null drop table [accounts]') + + await pool.request().query( + [ + 'create table [accounts] (', + ' [id] int primary key,', + ' [email] nvarchar(255) not null,', + ' [status] nvarchar(32) not null,', + ' [nickname] nvarchar(255) null', + ')', + ].join('\n'), + ) + + await pool.request().query( + [ + 'create table [projects] (', + ' [id] int primary key,', + ' [account_id] int not null,', + ' [name] nvarchar(255) not null,', + ' [archived] bit not null', + ')', + ].join('\n'), + ) + + await pool.request().query( + [ + 'create table [tasks] (', + ' [id] int primary key,', + ' [project_id] int not null,', + ' [title] nvarchar(255) not null,', + ' [state] nvarchar(32) not null', + ')', + ].join('\n'), + ) + }) + + after(async () => { + if (!integrationEnabled) { + return + } + + await pool.request().query('if object_id(\'tasks\', \'U\') is not null drop table [tasks]') + await pool.request().query('if object_id(\'projects\', \'U\') is not null drop table [projects]') + await pool.request().query('if object_id(\'accounts\', \'U\') is not null drop table [accounts]') + await pool.close() + }) + + runAdapterIntegrationContract({ + integrationEnabled, + createDatabase: () => createDatabase(createMssqlDatabaseAdapter(pool)), + resetDatabase: async () => { + await pool.request().query('delete from [tasks]') + await pool.request().query('delete from [projects]') + await pool.request().query('delete from [accounts]') + }, + }) + + // MSSQL-specific transaction tests: The shared integration contract does not + // verify that committed rows are visible or that rolled-back rows are discarded + // at the driver level, so these tests confirm the mssql driver's transaction + // semantics directly. + let txAccounts = createTable({ + name: 'accounts', + columns: { + id: number(), + email: string(), + status: string(), + nickname: nullable(string()), + }, + }) + + describe('transaction commit', () => { + beforeEach(async () => { + if (!integrationEnabled) return + await pool.request().query('delete from [accounts]') + }) + + it( + 'persists rows inserted inside a committed transaction', + { skip: !integrationEnabled }, + async () => { + let db = createDatabase(createMssqlDatabaseAdapter(pool)) + + await db.transaction(async tx => { + await tx.query(txAccounts).insertMany([ + { id: 1, email: 'a@test.com', status: 'active', nickname: null }, + ]) + }) + + let result = await pool.request().query('select [id] from [accounts] order by [id]') + assert.deepEqual( + result.recordset.map((r: { id: number }) => r.id), + [1], + 'row inserted inside committed transaction must be visible after commit', + ) + }, + ) + }) + + describe('transaction rollback', () => { + beforeEach(async () => { + if (!integrationEnabled) return + await pool.request().query('delete from [accounts]') + }) + + it( + 'discards rows inserted inside a rolled-back transaction', + { skip: !integrationEnabled }, + async () => { + let db = createDatabase(createMssqlDatabaseAdapter(pool)) + + await assert.rejects(() => + db.transaction(async tx => { + await tx.query(txAccounts).insertMany([ + { id: 2, email: 'b@test.com', status: 'active', nickname: null }, + ]) + throw new Error('forced rollback') + }), + ) + + let result = await pool.request().query('select [id] from [accounts] order by [id]') + assert.deepEqual( + result.recordset.map((r: { id: number }) => r.id), + [], + 'row inserted inside rolled-back transaction must not be visible after rollback', + ) + }, + ) + }) + + describe('transaction isolation level', () => { + beforeEach(async () => { + if (!integrationEnabled) return + await pool.request().query('delete from [accounts]') + }) + + it( + 'applies the requested isolation level without error', + { skip: !integrationEnabled }, + async () => { + let db = createDatabase(createMssqlDatabaseAdapter(pool)) + + await db.query(txAccounts).insertMany([ + { id: 1, email: 'a@test.com', status: 'active', nickname: null }, + { id: 2, email: 'b@test.com', status: 'active', nickname: null }, + ]) + + let count = await db.transaction( + async (tx) => tx.count(txAccounts), + { isolationLevel: 'serializable' }, + ) + + assert.equal(count, 2) + }, + ) + }) + + describe('limit and offset pagination', () => { + beforeEach(async () => { + if (!integrationEnabled) return + await pool.request().query('delete from [accounts]') + }) + + it( + 'paginates results with limit and offset', + { skip: !integrationEnabled }, + async () => { + let db = createDatabase(createMssqlDatabaseAdapter(pool)) + + await db.query(txAccounts).insertMany([ + { id: 1, email: 'a@test.com', status: 'active', nickname: null }, + { id: 2, email: 'b@test.com', status: 'active', nickname: null }, + { id: 3, email: 'c@test.com', status: 'active', nickname: null }, + { id: 4, email: 'd@test.com', status: 'active', nickname: null }, + ]) + + let page1 = await db.query(txAccounts).orderBy('id', 'asc').limit(2).offset(0).all() + + assert.deepEqual( + page1.map((r) => r.id), + [1, 2], + ) + + let page2 = await db.query(txAccounts).orderBy('id', 'asc').limit(2).offset(2).all() + + assert.deepEqual( + page2.map((r) => r.id), + [3, 4], + ) + }, + ) + }) +}) diff --git a/packages/data-table-mssql/src/lib/adapter.test.ts b/packages/data-table-mssql/src/lib/adapter.test.ts new file mode 100644 index 00000000000..073b1534bfc --- /dev/null +++ b/packages/data-table-mssql/src/lib/adapter.test.ts @@ -0,0 +1,572 @@ +import * as assert from 'node:assert/strict' +import { describe, it } from 'node:test' +import { number, string } from '@remix-run/data-schema' +import { createDatabase, createTable, eq, ilike, inList, sql } from '@remix-run/data-table' + +import { createMssqlDatabaseAdapter } from './adapter.ts' + +let accounts = createTable({ + name: 'accounts', + columns: { + id: number(), + email: string(), + }, +}) + +let projects = createTable({ + name: 'projects', + columns: { + id: number(), + account_id: number(), + name: string(), + }, +}) + +describe('mssql adapter', () => { + it('compiles ilike() with lower() and parses count results', async () => { + let statements: Array<{ text: string; values: unknown[] }> = [] + + let pool = { + request() { + let values: unknown[] = [] + + return { + input(_name: string, value: unknown) { + values.push(value) + return this + }, + async query(text: string) { + statements.push({ text, values: [...values] }) + return { + recordset: [{ count: '3' }], + rowsAffected: [1], + } + }, + } + }, + transaction() { + throw new Error('not used') + }, + } + + let db = createDatabase(createMssqlDatabaseAdapter(pool as never)) + + let count = await db.query(accounts).where(ilike('email', '%EXAMPLE%')).count() + + assert.equal(count, 3) + assert.match(statements[0].text, /lower\(\[email\]\) like lower\(@p1\)/) + assert.deepEqual(statements[0].values, ['%EXAMPLE%']) + }) + + it('starts and commits transactions on pooled connections', async () => { + let lifecycle: string[] = [] + + let transaction = { + async begin() { + lifecycle.push('begin') + }, + async commit() { + lifecycle.push('commit') + }, + async rollback() { + lifecycle.push('rollback') + }, + request() { + return { + input() { + return this + }, + async query() { + lifecycle.push('query') + return { + recordset: [], + rowsAffected: [1], + } + }, + } + }, + } + + let pool = { + request() { + throw new Error('unexpected root query') + }, + transaction() { + lifecycle.push('transaction') + return transaction + }, + } + + let db = createDatabase(createMssqlDatabaseAdapter(pool as never)) + + await db.transaction(async (transactionDatabase) => { + await transactionDatabase.query(accounts).insert({ id: 1, email: 'a@example.com' }) + }) + + assert.deepEqual(lifecycle, ['transaction', 'begin', 'query', 'commit']) + }) + + it('applies transaction options when provided', async () => { + let lifecycle: string[] = [] + + let transaction = { + async begin() { + lifecycle.push('begin') + }, + async commit() { + lifecycle.push('commit') + }, + async rollback() { + lifecycle.push('rollback') + }, + request() { + return { + input() { + return this + }, + async query(text: string) { + lifecycle.push(text) + return { + recordset: [], + rowsAffected: [0], + } + }, + } + }, + } + + let pool = { + request() { + throw new Error('not used') + }, + transaction() { + return transaction + }, + } + + let db = createDatabase(createMssqlDatabaseAdapter(pool as never)) + + await db.transaction(async () => undefined, { + isolationLevel: 'serializable', + readOnly: true, + }) + + assert.deepEqual(lifecycle, ['begin', 'set transaction isolation level serializable', 'set transaction read only', 'commit']) + }) + + it('compiles column-to-column comparisons from string references', async () => { + let statements: Array<{ text: string; values: unknown[] }> = [] + + let pool = { + request() { + let values: unknown[] = [] + + return { + input(_name: string, value: unknown) { + values.push(value) + return this + }, + async query(text: string) { + statements.push({ text, values: [...values] }) + + return { + recordset: [{ count: '0' }], + rowsAffected: [1], + } + }, + } + }, + transaction() { + throw new Error('not used') + }, + } + + let db = createDatabase(createMssqlDatabaseAdapter(pool as never)) + + await db + .query(accounts) + .join(projects, eq('accounts.id', 'projects.account_id')) + .where(eq('accounts.email', 'ops@example.com')) + .count() + + assert.match(statements[0].text, /\[accounts\]\.\[id\]\s*=\s*\[projects\]\.\[account_id\]/) + assert.match(statements[0].text, /\[accounts\]\.\[email\]\s*=\s*@p1/) + assert.deepEqual(statements[0].values, ['ops@example.com']) + }) + + it('does not create dangling bind parameters for inList predicates', async () => { + let statements: Array<{ text: string; values: unknown[] }> = [] + + let pool = { + request() { + let values: unknown[] = [] + + return { + input(_name: string, value: unknown) { + values.push(value) + return this + }, + async query(text: string) { + statements.push({ text, values: [...values] }) + + return { + recordset: [{ count: '0' }], + rowsAffected: [1], + } + }, + } + }, + transaction() { + throw new Error('not used') + }, + } + + let db = createDatabase(createMssqlDatabaseAdapter(pool as never)) + + await db + .query(accounts) + .where(inList('id', [1, 3])) + .count() + + assert.match(statements[0].text, /\[id\]\s+in\s+\(@p1,\s*@p2\)/) + assert.deepEqual(statements[0].values, [1, 3]) + }) + + it('loads the inserted row for create({ returnRow: true }) without RETURNING support', async () => { + let statements: Array<{ text: string; values: unknown[] }> = [] + let calls = 0 + + let pool = { + request() { + let values: unknown[] = [] + + return { + input(_name: string, value: unknown) { + values.push(value) + return this + }, + async query(text: string) { + calls += 1 + statements.push({ text, values: [...values] }) + + if (calls === 1) { + return { + recordset: [], + rowsAffected: [1], + } + } + + if (calls === 2) { + return { + recordset: [{ id: 2, email: 'fallback@example.com' }], + rowsAffected: [1], + } + } + + throw new Error('unexpected query call') + }, + } + }, + transaction() { + throw new Error('not used') + }, + } + + let db = createDatabase(createMssqlDatabaseAdapter(pool as never)) + + let created = await db.create( + accounts, + { + id: 2, + email: 'fallback@example.com', + }, + { returnRow: true }, + ) + + assert.equal(created.id, 2) + assert.equal(created.email, 'fallback@example.com') + assert.equal(statements.length, 2) + assert.match(statements[0].text, /^insert into \[accounts\]/) + assert.match(statements[1].text, /^select top \(1\) \* from \[accounts\]/) + assert.match(statements[1].text, /where \(\(\[id\] = @p1\)\)/) + assert.deepEqual(statements[1].values, [2]) + }) + + it('compiles upsert statements with merge using conflict targets', async () => { + let statements: Array<{ text: string; values: unknown[] }> = [] + + let pool = { + request() { + let values: unknown[] = [] + + return { + input(_name: string, value: unknown) { + values.push(value) + return this + }, + async query(text: string) { + statements.push({ text, values: [...values] }) + + return { + recordset: [], + rowsAffected: [1], + } + }, + } + }, + transaction() { + throw new Error('not used') + }, + } + + let db = createDatabase(createMssqlDatabaseAdapter(pool as never)) + + await db.query(accounts).upsert( + { + id: 7, + email: 'upsert@example.com', + }, + { + conflictTarget: ['id'], + }, + ) + + assert.match(statements[0].text, /^merge \[accounts\] with \(holdlock\) as target using \(values \(@p1, @p2\)\)/) + assert.match(statements[0].text, /on target\.\[id\] = source\.\[id\]/) + assert.match(statements[0].text, /when matched then update set target\.\[id\] = @p3, target\.\[email\] = @p4/) + assert.match(statements[0].text, /when not matched then insert \(\[id\], \[email\]\) values \(source\.\[id\], source\.\[email\]\)/) + assert.deepEqual(statements[0].values, [7, 'upsert@example.com', 7, 'upsert@example.com']) + }) + + it('rewrites raw sql ? placeholders to named @p1, @p2, … parameters', async () => { + let statements: Array<{ text: string; values: unknown[] }> = [] + + let pool = { + request() { + let values: unknown[] = [] + + return { + input(_name: string, value: unknown) { + values.push(value) + return this + }, + async query(text: string) { + statements.push({ text, values: [...values] }) + return { + recordset: [], + rowsAffected: [0], + } + }, + } + }, + transaction() { + throw new Error('not used') + }, + } + + let db = createDatabase(createMssqlDatabaseAdapter(pool as never)) + + await db.exec(sql`select * from accounts where id = ${42} and email = ${'a@example.com'}`) + + assert.equal(statements[0].text, 'select * from accounts where id = @p1 and email = @p2') + assert.deepEqual(statements[0].values, [42, 'a@example.com']) + }) + + it('uses T-SQL savepoints for nested transactions and omits release', async () => { + let lifecycle: string[] = [] + + let transaction = { + async begin() { + lifecycle.push('begin') + }, + async commit() { + lifecycle.push('commit') + }, + async rollback() { + lifecycle.push('rollback') + }, + request() { + return { + input() { + return this + }, + async query(text: string) { + lifecycle.push(text) + return { + recordset: [], + rowsAffected: [0], + } + }, + } + }, + } + + let pool = { + request() { + throw new Error('not used') + }, + transaction() { + lifecycle.push('transaction') + return transaction + }, + } + + let db = createDatabase(createMssqlDatabaseAdapter(pool as never)) + + await db.transaction(async (outerTx) => { + await outerTx + .transaction(async () => { + throw new Error('abort nested') + }) + .catch(() => undefined) + }) + + assert.deepEqual(lifecycle, [ + 'transaction', + 'begin', + 'save transaction [sp_0]', + 'rollback transaction [sp_0]', + // releaseSavepoint is a no-op for MSSQL — no RELEASE SAVEPOINT SQL + 'commit', + ]) + }) + + it('emits TOP (n) for limit-only selects without offset', async () => { + let statements: Array<{ text: string }> = [] + + let pool = { + request() { + return { + input() { + return this + }, + async query(text: string) { + statements.push({ text }) + return { + recordset: [], + rowsAffected: [0], + } + }, + } + }, + transaction() { + throw new Error('not used') + }, + } + + let db = createDatabase(createMssqlDatabaseAdapter(pool as never)) + + await db.query(accounts).limit(5).orderBy('id', 'asc').all() + + assert.match(statements[0].text, /^select top \(5\)/) + assert.doesNotMatch(statements[0].text, /offset/) + }) + + it('emits OFFSET…FETCH NEXT for limit + offset selects and omits TOP', async () => { + let statements: Array<{ text: string }> = [] + + let pool = { + request() { + return { + input() { + return this + }, + async query(text: string) { + statements.push({ text }) + return { + recordset: [], + rowsAffected: [0], + } + }, + } + }, + transaction() { + throw new Error('not used') + }, + } + + let db = createDatabase(createMssqlDatabaseAdapter(pool as never)) + + await db.query(accounts).limit(5).offset(10).orderBy('id', 'asc').all() + + assert.doesNotMatch(statements[0].text, /top/) + assert.match(statements[0].text, /offset 10 rows fetch next 5 rows only/) + }) + + it('injects ORDER BY (SELECT 1) when offset is used without an explicit orderBy', async () => { + let statements: Array<{ text: string }> = [] + + let pool = { + request() { + return { + input() { + return this + }, + async query(text: string) { + statements.push({ text }) + return { + recordset: [], + rowsAffected: [0], + } + }, + } + }, + transaction() { + throw new Error('not used') + }, + } + + let db = createDatabase(createMssqlDatabaseAdapter(pool as never)) + + await db.query(accounts).offset(10).all() + + assert.match(statements[0].text, /order by \(select 1\) offset 10 rows/) + assert.doesNotMatch(statements[0].text, /fetch next/) + }) + + it('uses OUTPUT inserted.* for insert when capabilities.returning is enabled', async () => { + let statements: Array<{ text: string; values: unknown[] }> = [] + let calls = 0 + + let pool = { + request() { + let values: unknown[] = [] + + return { + input(_name: string, value: unknown) { + values.push(value) + return this + }, + async query(text: string) { + calls += 1 + statements.push({ text, values: [...values] }) + return { + recordset: [{ id: 3, email: 'output@example.com' }], + rowsAffected: [1], + } + }, + } + }, + transaction() { + throw new Error('not used') + }, + } + + let db = createDatabase( + createMssqlDatabaseAdapter(pool as never, { capabilities: { returning: true } }), + ) + + let created = await db.create( + accounts, + { id: 3, email: 'output@example.com' }, + { returnRow: true }, + ) + + assert.equal(created.id, 3) + assert.equal(created.email, 'output@example.com') + // With returning enabled, a single INSERT…OUTPUT statement is issued (no fallback SELECT) + assert.equal(calls, 1) + assert.match(statements[0].text, /^insert into \[accounts\]/) + assert.match(statements[0].text, / output inserted\.\*/) + }) +}) diff --git a/packages/data-table-mssql/src/lib/adapter.ts b/packages/data-table-mssql/src/lib/adapter.ts new file mode 100644 index 00000000000..371a45d4c50 --- /dev/null +++ b/packages/data-table-mssql/src/lib/adapter.ts @@ -0,0 +1,309 @@ +import type { + AdapterCapabilityOverrides, + AdapterExecuteRequest, + AdapterResult, + DatabaseAdapter, + TransactionOptions, + TransactionToken, +} from '@remix-run/data-table' +import { getTablePrimaryKey } from '@remix-run/data-table' + +import { compileMssqlStatement } from './sql-compiler.ts' + +/** + * Result shape returned by mssql request `query()` calls. + */ +export type MssqlQueryResult = { + recordset?: unknown[] + rowsAffected?: number[] +} + +/** + * Minimal mssql request contract used by this adapter. + */ +export type MssqlDatabaseRequest = { + input(name: string, value: unknown): MssqlDatabaseRequest + query(text: string): Promise +} + +/** + * Minimal mssql queryable contract used by this adapter. + */ +type MssqlDatabaseQueryable = { + request(): MssqlDatabaseRequest +} + +/** + * Minimal mssql transaction contract used by this adapter. + */ +export type MssqlDatabaseTransaction = MssqlDatabaseQueryable & { + commit(): Promise + rollback(): Promise +} + +/** + * Internal handle returned by pool.transaction() before begin() is called. + */ +type MssqlTransactionHandle = MssqlDatabaseTransaction & { + begin(): Promise +} + +/** + * Minimal mssql pool contract used by this adapter. + */ +export type MssqlDatabasePool = MssqlDatabaseQueryable & { + transaction(): MssqlTransactionHandle +} + +/** + * Mssql adapter configuration. + */ +export type MssqlDatabaseAdapterOptions = { + capabilities?: AdapterCapabilityOverrides +} + +/** + * `DatabaseAdapter` implementation for mssql-compatible pools. + */ +export class MssqlDatabaseAdapter implements DatabaseAdapter { + dialect = 'mssql' + capabilities + + #client: MssqlDatabasePool + #transactions = new Map() + #transactionCounter = 0 + + constructor(client: MssqlDatabasePool, options?: MssqlDatabaseAdapterOptions) { + this.#client = client + this.capabilities = { + returning: options?.capabilities?.returning ?? false, + savepoints: options?.capabilities?.savepoints ?? true, + upsert: options?.capabilities?.upsert ?? true, + } + } + + async execute(request: AdapterExecuteRequest): Promise { + if (request.statement.kind === 'insertMany' && request.statement.values.length === 0) { + return { + affectedRows: 0, + insertId: undefined, + rows: request.statement.returning ? [] : undefined, + } + } + + let statement = compileMssqlStatement(request.statement) + let queryable = this.#resolveClient(request.transaction) + let result = await runMssqlQuery(queryable, statement.text, statement.values) + let rows = normalizeRows(result.recordset ?? []) + + if (request.statement.kind === 'count' || request.statement.kind === 'exists') { + rows = normalizeCountRows(rows) + } + + return { + rows, + affectedRows: normalizeAffectedRows(request.statement.kind, result.rowsAffected), + insertId: normalizeInsertId(request.statement.kind, request.statement, rows), + } + } + + async beginTransaction(options?: TransactionOptions): Promise { + let transaction = this.#client.transaction() + await transaction.begin() + + if (options?.isolationLevel) { + await runMssqlQuery(transaction, 'set transaction isolation level ' + options.isolationLevel) + } + + if (options?.readOnly !== undefined) { + await runMssqlQuery( + transaction, + options.readOnly ? 'set transaction read only' : 'set transaction read write', + ) + } + + this.#transactionCounter += 1 + let token = { id: 'tx_' + String(this.#transactionCounter) } + + this.#transactions.set(token.id, transaction) + + return token + } + + async commitTransaction(token: TransactionToken): Promise { + let transaction = this.#transactionClient(token) + + try { + await transaction.commit() + } finally { + this.#transactions.delete(token.id) + } + } + + async rollbackTransaction(token: TransactionToken): Promise { + let transaction = this.#transactionClient(token) + + try { + await transaction.rollback() + } finally { + this.#transactions.delete(token.id) + } + } + + async createSavepoint(token: TransactionToken, name: string): Promise { + let transaction = this.#transactionClient(token) + await runMssqlQuery(transaction, 'save transaction ' + quoteIdentifier(name)) + } + + async rollbackToSavepoint(token: TransactionToken, name: string): Promise { + let transaction = this.#transactionClient(token) + await runMssqlQuery(transaction, 'rollback transaction ' + quoteIdentifier(name)) + } + + // MSSQL does not support RELEASE SAVEPOINT — this is intentionally a no-op. + async releaseSavepoint(token: TransactionToken, _name: string): Promise { + this.#transactionClient(token) + } + + #resolveClient(token: TransactionToken | undefined): MssqlDatabaseQueryable { + if (!token) { + return this.#client + } + + return this.#transactionClient(token) + } + + #transactionClient(token: TransactionToken): MssqlDatabaseTransaction { + let transaction = this.#transactions.get(token.id) + + if (!transaction) { + throw new Error('Unknown transaction token: ' + token.id) + } + + return transaction + } +} + +/** + * Creates a mssql `DatabaseAdapter`. + * @param client Mssql connection pool. + * @param options Optional adapter capability overrides. + * @returns A configured mssql adapter. + */ +export function createMssqlDatabaseAdapter( + client: MssqlDatabasePool, + options?: MssqlDatabaseAdapterOptions, +): MssqlDatabaseAdapter { + return new MssqlDatabaseAdapter(client, options) +} + +async function runMssqlQuery( + queryable: MssqlDatabaseQueryable, + text: string, + values: unknown[] = [], +): Promise { + let request = queryable.request() + + for (let index = 0; index < values.length; index += 1) { + request.input('p' + String(index + 1), values[index]) + } + + return request.query(text) +} + +function normalizeRows(rows: unknown[]): Record[] { + return rows.map((row) => { + if (typeof row !== 'object' || row === null) { + return {} + } + + return { ...(row as Record) } + }) +} + +function normalizeCountRows(rows: Record[]): Record[] { + return rows.map((row) => { + let count = row.count + + if (typeof count === 'string') { + let numeric = Number(count) + + if (!Number.isNaN(numeric)) { + return { + ...row, + count: numeric, + } + } + } + + if (typeof count === 'bigint') { + return { + ...row, + count: Number(count), + } + } + + return row + }) +} + +function normalizeAffectedRows( + kind: AdapterExecuteRequest['statement']['kind'], + rowsAffected: number[] | undefined, +): number | undefined { + if (kind === 'select' || kind === 'count' || kind === 'exists' || kind === 'raw') { + return undefined + } + + if (!rowsAffected || rowsAffected.length === 0) { + return undefined + } + + let total = 0 + + for (let amount of rowsAffected) { + total += amount + } + + return total +} + +function normalizeInsertId( + kind: AdapterExecuteRequest['statement']['kind'], + statement: AdapterExecuteRequest['statement'], + rows: Record[], +): unknown { + if (!isInsertStatementKind(kind) || !isInsertStatement(statement)) { + return undefined + } + + let primaryKey = getTablePrimaryKey(statement.table) + + if (primaryKey.length !== 1) { + return undefined + } + + let key = primaryKey[0] + let row = rows[rows.length - 1] + + return row ? row[key] : undefined +} + +function quoteIdentifier(value: string): string { + return '[' + value.replace(/]/g, ']]') + ']' +} + +function isInsertStatementKind(kind: AdapterExecuteRequest['statement']['kind']): boolean { + return kind === 'insert' || kind === 'insertMany' || kind === 'upsert' +} + +function isInsertStatement( + statement: AdapterExecuteRequest['statement'], +): statement is Extract< + AdapterExecuteRequest['statement'], + { kind: 'insert' | 'insertMany' | 'upsert' } +> { + return ( + statement.kind === 'insert' || statement.kind === 'insertMany' || statement.kind === 'upsert' + ) +} diff --git a/packages/data-table-mssql/src/lib/sql-compiler.ts b/packages/data-table-mssql/src/lib/sql-compiler.ts new file mode 100644 index 00000000000..81940589b3d --- /dev/null +++ b/packages/data-table-mssql/src/lib/sql-compiler.ts @@ -0,0 +1,570 @@ +import { getTableName, getTablePrimaryKey } from '@remix-run/data-table' +import type { AdapterStatement, Predicate, SqlStatement } from '@remix-run/data-table' + +type JoinClause = Extract['joins'][number] +type UpsertStatement = Extract +type StatementTable = Extract['table'] + +type CompiledSql = { + text: string + values: unknown[] +} + +type CompileContext = { + values: unknown[] +} + +export function compileMssqlStatement(statement: AdapterStatement): CompiledSql { + if (statement.kind === 'raw') { + return compileRawStatement(statement.sql) + } + + let context: CompileContext = { values: [] } + + if (statement.kind === 'select') { + let selection = '*' + + if (statement.select !== '*') { + selection = statement.select + .map((field) => quotePath(field.column) + ' as ' + quoteIdentifier(field.alias)) + .join(', ') + } + + return { + text: + 'select ' + + (statement.distinct ? 'distinct ' : '') + + compileTopClause(statement.limit, statement.offset) + + selection + + compileFromClause(statement.table, statement.joins, context) + + compileWhereClause(statement.where, context) + + compileGroupByClause(statement.groupBy) + + compileHavingClause(statement.having, context) + + compileOrderByClause(statement.orderBy) + + compileOffsetClause(statement.orderBy.length > 0, statement.limit, statement.offset), + values: context.values, + } + } + + if (statement.kind === 'count' || statement.kind === 'exists') { + let inner = + 'select 1 as ' + + quoteIdentifier('__dt_col') + + compileFromClause(statement.table, statement.joins, context) + + compileWhereClause(statement.where, context) + + compileGroupByClause(statement.groupBy) + + compileHavingClause(statement.having, context) + + return { + text: + 'select count(*) as ' + + quoteIdentifier('count') + + ' from (' + + inner + + ') as ' + + quoteIdentifier('__dt_count'), + values: context.values, + } + } + + if (statement.kind === 'insert') { + return compileInsertStatement(statement.table, statement.values, statement.returning, context) + } + + if (statement.kind === 'insertMany') { + return compileInsertManyStatement(statement.table, statement.values, statement.returning, context) + } + + if (statement.kind === 'update') { + let columns = Object.keys(statement.changes) + + return { + text: + 'update ' + + quoteIdentifier(getTableName(statement.table)) + + ' set ' + + columns + .map((column) => quotePath(column) + ' = ' + pushValue(context, statement.changes[column])) + .join(', ') + + compileOutputClause(statement.returning, 'inserted') + + compileWhereClause(statement.where, context), + values: context.values, + } + } + + if (statement.kind === 'delete') { + return { + text: + 'delete from ' + + quoteIdentifier(getTableName(statement.table)) + + compileOutputClause(statement.returning, 'deleted') + + compileWhereClause(statement.where, context), + values: context.values, + } + } + + if (statement.kind === 'upsert') { + return compileUpsertStatement(statement, context) + } + + throw new Error('Unsupported statement kind') +} + +function compileInsertStatement( + table: StatementTable, + values: Record, + returning: '*' | string[] | undefined, + context: CompileContext, +): CompiledSql { + let columns = Object.keys(values) + + if (columns.length === 0) { + return { + text: + 'insert into ' + + quoteIdentifier(getTableName(table)) + + compileOutputClause(returning, 'inserted') + + ' default values', + values: context.values, + } + } + + return { + text: + 'insert into ' + + quoteIdentifier(getTableName(table)) + + ' (' + + columns.map((column) => quotePath(column)).join(', ') + + ')' + + compileOutputClause(returning, 'inserted') + + ' values (' + + columns.map((column) => pushValue(context, values[column])).join(', ') + + ')', + values: context.values, + } +} + +function compileInsertManyStatement( + table: StatementTable, + rows: Record[], + returning: '*' | string[] | undefined, + context: CompileContext, +): CompiledSql { + if (rows.length === 0) { + return { + text: 'select 0 where 1 = 0', + values: context.values, + } + } + + let columns = collectColumns(rows) + + if (columns.length === 0) { + return { + text: + 'insert into ' + + quoteIdentifier(getTableName(table)) + + compileOutputClause(returning, 'inserted') + + ' default values', + values: context.values, + } + } + + return { + text: + 'insert into ' + + quoteIdentifier(getTableName(table)) + + ' (' + + columns.map((column) => quotePath(column)).join(', ') + + ')' + + compileOutputClause(returning, 'inserted') + + ' values ' + + rows + .map( + (row) => + '(' + + columns + .map((column) => { + let value = Object.prototype.hasOwnProperty.call(row, column) ? row[column] : null + return pushValue(context, value) + }) + .join(', ') + + ')', + ) + .join(', '), + values: context.values, + } +} + +function compileUpsertStatement(statement: UpsertStatement, context: CompileContext): CompiledSql { + let insertColumns = Object.keys(statement.values) + + if (insertColumns.length === 0) { + throw new Error('upsert requires at least one value') + } + + let conflictTarget = statement.conflictTarget ?? [...getTablePrimaryKey(statement.table)] + + if (conflictTarget.length === 0) { + throw new Error('upsert requires at least one conflict target column') + } + + let sourceValues = insertColumns.map((column) => pushValue(context, statement.values[column])) + + let updateValues = statement.update ?? statement.values + let updateColumns = Object.keys(updateValues) + let whenMatchedClause = '' + + if (updateColumns.length > 0) { + whenMatchedClause = + ' when matched then update set ' + + updateColumns + .map((column) => 'target.' + quotePath(column) + ' = ' + pushValue(context, updateValues[column])) + .join(', ') + } + + return { + text: + 'merge ' + + quoteIdentifier(getTableName(statement.table)) + + ' with (holdlock) as target using (values (' + + sourceValues.join(', ') + + ')) as source (' + + insertColumns.map((column) => quotePath(column)).join(', ') + + ') on ' + + conflictTarget + .map((column) => 'target.' + quotePath(column) + ' = source.' + quotePath(column)) + .join(' and ') + + whenMatchedClause + + ' when not matched then insert (' + + insertColumns.map((column) => quotePath(column)).join(', ') + + ') values (' + + insertColumns.map((column) => 'source.' + quotePath(column)).join(', ') + + ')' + + compileOutputClause(statement.returning, 'inserted') + + ';', + values: context.values, + } +} + +function compileRawStatement(statement: SqlStatement): CompiledSql { + if (!statement.text.includes('?')) { + return { + text: statement.text, + values: [...statement.values], + } + } + + let index = 1 + let text = statement.text.replace(/\?/g, function replaceParameter() { + let placeholder = '@p' + String(index) + index += 1 + return placeholder + }) + + return { + text, + values: [...statement.values], + } +} + +function compileFromClause( + table: StatementTable, + joins: JoinClause[], + context: CompileContext, +): string { + let output = ' from ' + quoteIdentifier(getTableName(table)) + + for (let join of joins) { + output += + ' ' + + normalizeJoinType(join.type) + + ' join ' + + quoteIdentifier(getTableName(join.table)) + + ' on ' + + compilePredicate(join.on, context) + } + + return output +} + +function compileWhereClause(predicates: Predicate[], context: CompileContext): string { + if (predicates.length === 0) { + return '' + } + + return ( + ' where ' + + predicates.map((predicate) => '(' + compilePredicate(predicate, context) + ')').join(' and ') + ) +} + +function compileGroupByClause(columns: string[]): string { + if (columns.length === 0) { + return '' + } + + return ' group by ' + columns.map((column) => quotePath(column)).join(', ') +} + +function compileHavingClause(predicates: Predicate[], context: CompileContext): string { + if (predicates.length === 0) { + return '' + } + + return ( + ' having ' + + predicates.map((predicate) => '(' + compilePredicate(predicate, context) + ')').join(' and ') + ) +} + +function compileOrderByClause(orderBy: { column: string; direction: 'asc' | 'desc' }[]): string { + if (orderBy.length === 0) { + return '' + } + + return ( + ' order by ' + + orderBy + .map((clause) => quotePath(clause.column) + ' ' + clause.direction.toUpperCase()) + .join(', ') + ) +} + +function compileTopClause(limit: number | undefined, offset: number | undefined): string { + if (limit === undefined || offset !== undefined) { + return '' + } + + return 'top (' + String(limit) + ') ' +} + +function compileOffsetClause( + hasOrderBy: boolean, + limit: number | undefined, + offset: number | undefined, +): string { + if (offset === undefined) { + return '' + } + + let output = '' + + if (!hasOrderBy) { + output += ' order by (select 1)' + } + + output += ' offset ' + String(offset) + ' rows' + + if (limit !== undefined) { + output += ' fetch next ' + String(limit) + ' rows only' + } + + return output +} + +function compileOutputClause( + returning: '*' | string[] | undefined, + tableAlias: 'inserted' | 'deleted', +): string { + if (!returning) { + return '' + } + + if (returning === '*') { + return ' output ' + tableAlias + '.*' + } + + return ( + ' output ' + + returning + .map((column) => { + if (column.includes('.')) { + return quotePath(column) + } + + return tableAlias + '.' + quoteIdentifier(column) + }) + .join(', ') + ) +} + +function compilePredicate(predicate: Predicate, context: CompileContext): string { + if (predicate.type === 'comparison') { + let column = quotePath(predicate.column) + + if (predicate.operator === 'eq') { + if ( + predicate.valueType === 'value' && + (predicate.value === null || predicate.value === undefined) + ) { + return column + ' is null' + } + + let comparisonValue = compileComparisonValue(predicate, context) + return column + ' = ' + comparisonValue + } + + if (predicate.operator === 'ne') { + if ( + predicate.valueType === 'value' && + (predicate.value === null || predicate.value === undefined) + ) { + return column + ' is not null' + } + + let comparisonValue = compileComparisonValue(predicate, context) + return column + ' <> ' + comparisonValue + } + + if (predicate.operator === 'gt') { + let comparisonValue = compileComparisonValue(predicate, context) + return column + ' > ' + comparisonValue + } + + if (predicate.operator === 'gte') { + let comparisonValue = compileComparisonValue(predicate, context) + return column + ' >= ' + comparisonValue + } + + if (predicate.operator === 'lt') { + let comparisonValue = compileComparisonValue(predicate, context) + return column + ' < ' + comparisonValue + } + + if (predicate.operator === 'lte') { + let comparisonValue = compileComparisonValue(predicate, context) + return column + ' <= ' + comparisonValue + } + + if (predicate.operator === 'in' || predicate.operator === 'notIn') { + let values = Array.isArray(predicate.value) ? predicate.value : [] + + if (values.length === 0) { + return predicate.operator === 'in' ? '1 = 0' : '1 = 1' + } + + let keyword = predicate.operator === 'in' ? 'in' : 'not in' + + return ( + column + + ' ' + + keyword + + ' (' + + values.map((value) => pushValue(context, value)).join(', ') + + ')' + ) + } + + if (predicate.operator === 'like') { + let comparisonValue = compileComparisonValue(predicate, context) + return column + ' like ' + comparisonValue + } + + if (predicate.operator === 'ilike') { + let comparisonValue = compileComparisonValue(predicate, context) + return 'lower(' + column + ') like lower(' + comparisonValue + ')' + } + } + + if (predicate.type === 'between') { + return ( + quotePath(predicate.column) + + ' between ' + + pushValue(context, predicate.lower) + + ' and ' + + pushValue(context, predicate.upper) + ) + } + + if (predicate.type === 'null') { + return ( + quotePath(predicate.column) + (predicate.operator === 'isNull' ? ' is null' : ' is not null') + ) + } + + if (predicate.type === 'logical') { + if (predicate.predicates.length === 0) { + return predicate.operator === 'and' ? '1 = 1' : '1 = 0' + } + + let joiner = predicate.operator === 'and' ? ' and ' : ' or ' + + return predicate.predicates + .map((child) => '(' + compilePredicate(child, context) + ')') + .join(joiner) + } + + throw new Error('Unsupported predicate') +} + +function compileComparisonValue( + predicate: Extract, + context: CompileContext, +): string { + if (predicate.valueType === 'column') { + return quotePath(predicate.value) + } + + return pushValue(context, predicate.value) +} + +function normalizeJoinType(type: string): string { + if (type === 'left') { + return 'left' + } + + if (type === 'right') { + return 'right' + } + + return 'inner' +} + +function quoteIdentifier(value: string): string { + return '[' + value.replace(/]/g, ']]') + ']' +} + +function quotePath(path: string): string { + if (path === '*') { + return '*' + } + + return path + .split('.') + .map((segment) => { + if (segment === '*') { + return '*' + } + + return quoteIdentifier(segment) + }) + .join('.') +} + +function pushValue(context: CompileContext, value: unknown): string { + context.values.push(value) + return '@p' + String(context.values.length) +} + +function collectColumns(rows: Record[]): string[] { + let columns: string[] = [] + let seen = new Set() + + for (let row of rows) { + for (let key in row) { + if (!Object.prototype.hasOwnProperty.call(row, key)) { + continue + } + + if (seen.has(key)) { + continue + } + + seen.add(key) + columns.push(key) + } + } + + return columns +} diff --git a/packages/data-table-mssql/tsconfig.build.json b/packages/data-table-mssql/tsconfig.build.json new file mode 100644 index 00000000000..f9f2e5e8932 --- /dev/null +++ b/packages/data-table-mssql/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "outDir": "./dist" + }, + "include": ["src"], + "exclude": ["src/**/*.test.ts"] +} diff --git a/packages/data-table-mssql/tsconfig.json b/packages/data-table-mssql/tsconfig.json new file mode 100644 index 00000000000..43aef5a7aa6 --- /dev/null +++ b/packages/data-table-mssql/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "strict": true, + "lib": ["ES2024", "DOM", "DOM.Iterable"], + "module": "ES2022", + "moduleResolution": "Bundler", + "target": "ESNext", + "types": ["node"], + "allowImportingTsExtensions": true, + "rewriteRelativeImportExtensions": true, + "verbatimModuleSyntax": true + }, + "exclude": ["vendor"] +} diff --git a/packages/remix/.changes/minor.remix.update-exports-data-table-mssql.md b/packages/remix/.changes/minor.remix.update-exports-data-table-mssql.md new file mode 100644 index 00000000000..12f8eac69d8 --- /dev/null +++ b/packages/remix/.changes/minor.remix.update-exports-data-table-mssql.md @@ -0,0 +1,3 @@ +Added `package.json` `exports`: + +- `remix/data-table-mssql` to re-export APIs from `@remix-run/data-table-mssql` diff --git a/packages/remix/package.json b/packages/remix/package.json index 1d07b93b993..4f0edb1455a 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -30,6 +30,7 @@ "./data-schema/coerce": "./src/data-schema/coerce.ts", "./data-schema/lazy": "./src/data-schema/lazy.ts", "./data-table": "./src/data-table.ts", + "./data-table-mssql": "./src/data-table-mssql.ts", "./data-table-mysql": "./src/data-table-mysql.ts", "./data-table-postgres": "./src/data-table-postgres.ts", "./data-table-sqlite": "./src/data-table-sqlite.ts", @@ -123,6 +124,10 @@ "types": "./dist/data-table.d.ts", "default": "./dist/data-table.js" }, + "./data-table-mssql": { + "types": "./dist/data-table-mssql.d.ts", + "default": "./dist/data-table-mssql.js" + }, "./data-table-mysql": { "types": "./dist/data-table-mysql.d.ts", "default": "./dist/data-table-mysql.js" @@ -322,6 +327,7 @@ "@remix-run/tar-parser": "workspace:^", "@remix-run/data-schema": "workspace:^", "@remix-run/data-table": "workspace:^", + "@remix-run/data-table-mssql": "workspace:^", "@remix-run/data-table-mysql": "workspace:^", "@remix-run/data-table-postgres": "workspace:^", "@remix-run/data-table-sqlite": "workspace:^" diff --git a/packages/remix/src/data-table-mssql.ts b/packages/remix/src/data-table-mssql.ts new file mode 100644 index 00000000000..9403ec213da --- /dev/null +++ b/packages/remix/src/data-table-mssql.ts @@ -0,0 +1,2 @@ +// IMPORTANT: This file is auto-generated, please do not edit manually. +export * from '@remix-run/data-table-mssql' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e39e298dd55..b2d9905f928 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -384,6 +384,28 @@ importers: specifier: ^12.4.1 version: 12.6.2 + packages/data-table-mssql: + dependencies: + '@remix-run/data-table': + specifier: workspace:^ + version: link:../data-table + devDependencies: + '@remix-run/data-schema': + specifier: workspace:* + version: link:../data-schema + '@types/mssql': + specifier: ^9.1.8 + version: 9.1.9(@azure/core-client@1.10.1) + '@types/node': + specifier: 'catalog:' + version: 24.10.4 + '@typescript/native-preview': + specifier: 'catalog:' + version: 7.0.0-dev.20251125.1 + mssql: + specifier: ^12.2.0 + version: 12.2.0(@azure/core-client@1.10.1) + packages/data-table-mysql: dependencies: '@remix-run/data-table': @@ -939,6 +961,9 @@ importers: '@remix-run/data-table': specifier: workspace:^ version: link:../data-table + '@remix-run/data-table-mssql': + specifier: workspace:^ + version: link:../data-table-mssql '@remix-run/data-table-mysql': specifier: workspace:^ version: link:../data-table-mysql @@ -1225,6 +1250,77 @@ packages: '@ark/util@0.56.0': resolution: {integrity: sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA==} + '@azure-rest/core-client@2.5.1': + resolution: {integrity: sha512-EHaOXW0RYDKS5CFffnixdyRPak5ytiCtU7uXDcP/uiY+A6jFRwNGzzJBiznkCzvi5EYpY+YWinieqHb0oY916A==} + engines: {node: '>=20.0.0'} + + '@azure/abort-controller@2.1.2': + resolution: {integrity: sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==} + engines: {node: '>=18.0.0'} + + '@azure/core-auth@1.10.1': + resolution: {integrity: sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==} + engines: {node: '>=20.0.0'} + + '@azure/core-client@1.10.1': + resolution: {integrity: sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==} + engines: {node: '>=20.0.0'} + + '@azure/core-http-compat@2.3.2': + resolution: {integrity: sha512-Tf6ltdKzOJEgxZeWLCjMxrxbodB/ZeCbzzA1A2qHbhzAjzjHoBVSUeSl/baT/oHAxhc4qdqVaDKnc2+iE932gw==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@azure/core-client': ^1.10.0 + '@azure/core-rest-pipeline': ^1.22.0 + + '@azure/core-lro@2.7.2': + resolution: {integrity: sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==} + engines: {node: '>=18.0.0'} + + '@azure/core-paging@1.6.2': + resolution: {integrity: sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==} + engines: {node: '>=18.0.0'} + + '@azure/core-rest-pipeline@1.22.2': + resolution: {integrity: sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg==} + engines: {node: '>=20.0.0'} + + '@azure/core-tracing@1.3.1': + resolution: {integrity: sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==} + engines: {node: '>=20.0.0'} + + '@azure/core-util@1.13.1': + resolution: {integrity: sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==} + engines: {node: '>=20.0.0'} + + '@azure/identity@4.13.0': + resolution: {integrity: sha512-uWC0fssc+hs1TGGVkkghiaFkkS7NkTxfnCH+Hdg+yTehTpMcehpok4PgUKKdyCH+9ldu6FhiHRv84Ntqj1vVcw==} + engines: {node: '>=20.0.0'} + + '@azure/keyvault-common@2.0.0': + resolution: {integrity: sha512-wRLVaroQtOqfg60cxkzUkGKrKMsCP6uYXAOomOIysSMyt1/YM0eUn9LqieAWM8DLcU4+07Fio2YGpPeqUbpP9w==} + engines: {node: '>=18.0.0'} + + '@azure/keyvault-keys@4.10.0': + resolution: {integrity: sha512-eDT7iXoBTRZ2n3fLiftuGJFD+yjkiB1GNqzU2KbY1TLYeXeSPVTVgn2eJ5vmRTZ11978jy2Kg2wI7xa9Tyr8ag==} + engines: {node: '>=18.0.0'} + + '@azure/logger@1.3.0': + resolution: {integrity: sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==} + engines: {node: '>=20.0.0'} + + '@azure/msal-browser@4.29.0': + resolution: {integrity: sha512-/f3eHkSNUTl6DLQHm+bKecjBKcRQxbd/XLx8lvSYp8Nl/HRyPuIPOijt9Dt0sH50/SxOwQ62RnFCmFlGK+bR/w==} + engines: {node: '>=0.8.0'} + + '@azure/msal-common@15.15.0': + resolution: {integrity: sha512-/n+bN0AKlVa+AOcETkJSKj38+bvFs78BaP4rNtv3MJCmPH0YrHiskMRe74OhyZ5DZjGISlFyxqvf9/4QVEi2tw==} + engines: {node: '>=0.8.0'} + + '@azure/msal-node@3.8.8': + resolution: {integrity: sha512-+f1VrJH1iI517t4zgmuhqORja0bL6LDQXfBqkjuMmfTYXTQQnh1EvwwxO3UbKLT05N0obF72SRHFrC1RBDv5Gg==} + engines: {node: '>=16'} + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -2086,6 +2182,9 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@js-joda/core@5.7.0': + resolution: {integrity: sha512-WBu4ULVVxySLLzK1Ppq+OdfP+adRS4ntmDQT915rzDJ++i95gc2jZkM5B6LWEAwN3lGXpfie3yPABozdD3K3Vg==} + '@octokit/endpoint@10.1.1': resolution: {integrity: sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==} engines: {node: '>= 18'} @@ -2284,6 +2383,9 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@tediousjs/connection-string@0.6.0': + resolution: {integrity: sha512-GxlsW354Vi6QqbUgdPyQVcQjI7cZBdGV5vOYVYuCVDTylx2wl3WHR2HlhcxxHTrMigbelpXsdcZso+66uxPfow==} + '@testing-library/dom@10.4.1': resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} @@ -2345,6 +2447,9 @@ packages: '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + '@types/mssql@9.1.9': + resolution: {integrity: sha512-P0nCgw6vzY23UxZMnbI4N7fnLGANt4LI4yvxze1paPj+LuN28cFv5EI+QidP8udnId/BKhkcRhm/BleNsjK65A==} + '@types/node@20.12.14': resolution: {integrity: sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg==} @@ -2368,6 +2473,9 @@ packages: '@types/react@19.2.7': resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} + '@types/readable-stream@4.0.23': + resolution: {integrity: sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig==} + '@types/semver@7.5.8': resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} @@ -2499,6 +2607,10 @@ packages: peerDependencies: typescript: '*' + '@typespec/ts-http-runtime@0.3.3': + resolution: {integrity: sha512-91fp6CAAJSRtH5ja95T1FHSKa8aPW9/Zw6cta81jlZTUw/+Vq8jM/AfF/14h2b71wwR84JUTW/3Y8QPhDAawFA==} + engines: {node: '>=20.0.0'} + '@vitest/browser@3.2.4': resolution: {integrity: sha512-tJxiPrWmzH8a+w9nLKlQMzAKX/7VjFs50MWgcAj7p9XQ7AQ9/35fByFYptgPELyLw+0aixTnC4pUWV+APcZ/kw==} peerDependencies: @@ -2575,6 +2687,10 @@ packages: '@zeit/schemas@2.36.0': resolution: {integrity: sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==} + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -2598,6 +2714,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -2735,6 +2855,9 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + bl@6.1.6: + resolution: {integrity: sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==} + blake3-wasm@2.1.5: resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} @@ -2760,15 +2883,25 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + bun-types@1.1.26: resolution: {integrity: sha512-n7jDe62LsB2+WE8Q8/mT3azkPaatKlj/2MyP6hi3mKvPz9oPpB6JW/Ll6JHtNLudasFFuvfgklYSE+rreGvBjw==} + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} @@ -2872,6 +3005,10 @@ packages: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} + commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + comment-parser@1.4.1: resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==} engines: {node: '>= 12.0.0'} @@ -2992,10 +3129,22 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + engines: {node: '>=18'} + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + define-properties@1.2.1: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} @@ -3037,6 +3186,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -3232,6 +3384,14 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -3481,6 +3641,14 @@ packages: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -3569,6 +3737,11 @@ packages: engines: {node: '>=8'} hasBin: true + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -3593,6 +3766,11 @@ packages: resolution: {integrity: sha512-rcfALRIb1YewtnksfRIHGcIY93QnK8BIQ/2c9yDYcG/Y6+vRoJuTWBmmSEbyLLYtXm7q35pHOHbZFQBaLrhlWQ==} engines: {node: '>=0.10.0'} + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} @@ -3656,6 +3834,10 @@ packages: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} @@ -3668,6 +3850,9 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + js-md4@0.3.2: + resolution: {integrity: sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -3721,6 +3906,16 @@ packages: engines: {node: '>=10'} hasBin: true + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -3739,9 +3934,30 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} @@ -3904,6 +4120,11 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + mssql@12.2.0: + resolution: {integrity: sha512-lwwLHAqcWOz8okjboQpIEp5OghUFGJhuuQZS3+WF1ZXbaEaCEGKOfiQET3w/5Xz0tyZfDNCQVCm9wp5GwXut6g==} + engines: {node: '>=18'} + hasBin: true + multipasta@0.2.5: resolution: {integrity: sha512-c8eMDb1WwZcE02WVjHoOmUVk7fnKU/RmUcosHACglrWAuPQsEJv+E8430sXj6jNc1jHw0zrS16aCjQh4BcEb4A==} @@ -3923,6 +4144,9 @@ packages: napi-build-utils@2.0.0: resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + native-duplexpair@1.0.0: + resolution: {integrity: sha512-E7QQoM+3jvNtlmyfqRZ0/U75VFgCls+fSkbml2MpgWkWyz3ox8Y58gNhfuziuQYGNNQAbFZJQck55LHCnCK6CA==} + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -3994,6 +4218,10 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + open@10.2.0: + resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} + engines: {node: '>=18'} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -4182,6 +4410,10 @@ packages: process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -4245,6 +4477,10 @@ packages: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} + readable-stream@4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -4301,6 +4537,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} @@ -4474,6 +4714,9 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + sqlstring@2.3.3: resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} engines: {node: '>= 0.6'} @@ -4529,6 +4772,9 @@ packages: string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -4587,6 +4833,14 @@ packages: engines: {node: '>=18'} deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + tarn@3.0.2: + resolution: {integrity: sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==} + engines: {node: '>=8.0.0'} + + tedious@19.2.1: + resolution: {integrity: sha512-pk1Q16Yl62iocuQB+RWbg6rFUFkIyzqOFQ6NfysCltRvQqKwfurgj8v/f2X+CKvDhSL4IJ0cCOfCHDg9PWEEYA==} + engines: {node: '>=18.17'} + text-decoder@1.2.1: resolution: {integrity: sha512-x9v3H/lTKIJKQQe7RPQkLfKAnc9lUTkWDypIQgTzPJAq+5/GCDHonmshfvlsNSj58yyshbIJJDLmU15qNERrXQ==} @@ -4662,6 +4916,9 @@ packages: tslib@2.7.0: resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.21.0: resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} engines: {node: '>=18.0.0'} @@ -4763,6 +5020,10 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -4959,6 +5220,10 @@ packages: utf-8-validate: optional: true + wsl-utils@0.1.0: + resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} + engines: {node: '>=18'} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -5031,6 +5296,150 @@ snapshots: '@ark/util@0.56.0': {} + '@azure-rest/core-client@2.5.1': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-rest-pipeline': 1.22.2 + '@azure/core-tracing': 1.3.1 + '@typespec/ts-http-runtime': 0.3.3 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/abort-controller@2.1.2': + dependencies: + tslib: 2.8.1 + + '@azure/core-auth@1.10.1': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-util': 1.13.1 + tslib: 2.7.0 + transitivePeerDependencies: + - supports-color + + '@azure/core-client@1.10.1': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-rest-pipeline': 1.22.2 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + tslib: 2.7.0 + transitivePeerDependencies: + - supports-color + + '@azure/core-http-compat@2.3.2(@azure/core-client@1.10.1)(@azure/core-rest-pipeline@1.22.2)': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-client': 1.10.1 + '@azure/core-rest-pipeline': 1.22.2 + + '@azure/core-lro@2.7.2': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-paging@1.6.2': + dependencies: + tslib: 2.8.1 + + '@azure/core-rest-pipeline@1.22.2': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + '@typespec/ts-http-runtime': 0.3.3 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-tracing@1.3.1': + dependencies: + tslib: 2.8.1 + + '@azure/core-util@1.13.1': + dependencies: + '@azure/abort-controller': 2.1.2 + '@typespec/ts-http-runtime': 0.3.3 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/identity@4.13.0': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-client': 1.10.1 + '@azure/core-rest-pipeline': 1.22.2 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + '@azure/msal-browser': 4.29.0 + '@azure/msal-node': 3.8.8 + open: 10.2.0 + tslib: 2.7.0 + transitivePeerDependencies: + - supports-color + + '@azure/keyvault-common@2.0.0': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-client': 1.10.1 + '@azure/core-rest-pipeline': 1.22.2 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/keyvault-keys@4.10.0(@azure/core-client@1.10.1)': + dependencies: + '@azure-rest/core-client': 2.5.1 + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-http-compat': 2.3.2(@azure/core-client@1.10.1)(@azure/core-rest-pipeline@1.22.2) + '@azure/core-lro': 2.7.2 + '@azure/core-paging': 1.6.2 + '@azure/core-rest-pipeline': 1.22.2 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/keyvault-common': 2.0.0 + '@azure/logger': 1.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - '@azure/core-client' + - supports-color + + '@azure/logger@1.3.0': + dependencies: + '@typespec/ts-http-runtime': 0.3.3 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/msal-browser@4.29.0': + dependencies: + '@azure/msal-common': 15.15.0 + + '@azure/msal-common@15.15.0': {} + + '@azure/msal-node@3.8.8': + dependencies: + '@azure/msal-common': 15.15.0 + jsonwebtoken: 9.0.3 + uuid: 8.3.2 + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -5684,6 +6093,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@js-joda/core@5.7.0': {} + '@octokit/endpoint@10.1.1': dependencies: '@octokit/types': 13.6.2 @@ -5840,6 +6251,8 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@tediousjs/connection-string@0.6.0': {} + '@testing-library/dom@10.4.1': dependencies: '@babel/code-frame': 7.27.1 @@ -5917,6 +6330,15 @@ snapshots: '@types/mime@1.3.5': {} + '@types/mssql@9.1.9(@azure/core-client@1.10.1)': + dependencies: + '@types/node': 24.10.4 + tarn: 3.0.2 + tedious: 19.2.1(@azure/core-client@1.10.1) + transitivePeerDependencies: + - '@azure/core-client' + - supports-color + '@types/node@20.12.14': dependencies: undici-types: 5.26.5 @@ -5943,6 +6365,10 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/readable-stream@4.0.23': + dependencies: + '@types/node': 24.10.4 + '@types/semver@7.5.8': {} '@types/send@0.17.4': @@ -6113,6 +6539,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@typespec/ts-http-runtime@0.3.3': + dependencies: + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + '@vitest/browser@3.2.4(playwright@1.57.0)(vite@7.3.0(@types/node@24.10.4)(tsx@4.21.0)(yaml@2.8.2))(vitest@3.2.4)': dependencies: '@testing-library/dom': 10.4.1 @@ -6215,6 +6649,10 @@ snapshots: '@zeit/schemas@2.36.0': {} + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -6230,6 +6668,8 @@ snapshots: acorn@8.15.0: {} + agent-base@7.1.4: {} + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -6390,6 +6830,13 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + bl@6.1.6: + dependencies: + '@types/readable-stream': 4.0.23 + buffer: 6.0.3 + inherits: 2.0.4 + readable-stream: 4.7.0 + blake3-wasm@2.1.5: {} body-parser@1.20.2: @@ -6441,6 +6888,8 @@ snapshots: node-releases: 2.0.27 update-browserslist-db: 1.2.3(browserslist@4.28.1) + buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} buffer@5.7.1: @@ -6448,11 +6897,20 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + bun-types@1.1.26: dependencies: '@types/node': 20.12.14 '@types/ws': 8.5.12 + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + busboy@1.6.0: dependencies: streamsearch: 1.1.0 @@ -6557,6 +7015,8 @@ snapshots: color-convert: 2.0.1 color-string: 1.9.1 + commander@11.1.0: {} + comment-parser@1.4.1: {} compressible@2.0.18: @@ -6660,12 +7120,21 @@ snapshots: deep-is@0.1.4: {} + default-browser-id@5.0.1: {} + + default-browser@5.5.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + define-data-property@1.1.4: dependencies: es-define-property: 1.0.0 es-errors: 1.3.0 gopd: 1.0.1 + define-lazy-prop@3.0.0: {} + define-properties@1.2.1: dependencies: define-data-property: 1.1.4 @@ -6703,6 +7172,10 @@ snapshots: eastasianwidth@0.2.0: {} + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + ee-first@1.1.1: {} electron-to-chromium@1.5.267: {} @@ -7059,6 +7532,10 @@ snapshots: etag@1.8.1: {} + event-target-shim@5.0.1: {} + + events@3.3.0: {} + execa@5.1.1: dependencies: cross-spawn: 7.0.5 @@ -7341,6 +7818,20 @@ snapshots: statuses: 2.0.1 toidentifier: 1.0.1 + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + human-signals@2.1.0: {} iconv-lite@0.4.24: @@ -7422,6 +7913,8 @@ snapshots: is-docker@2.2.1: {} + is-docker@3.0.0: {} + is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: @@ -7444,6 +7937,10 @@ snapshots: is-gzip@1.0.0: {} + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + is-map@2.0.3: {} is-negative-zero@2.0.3: {} @@ -7502,6 +7999,10 @@ snapshots: dependencies: is-docker: 2.2.1 + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + isarray@1.0.0: {} isarray@2.0.5: {} @@ -7514,6 +8015,8 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 + js-md4@0.3.2: {} + js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -7552,6 +8055,30 @@ snapshots: jsonparse: 1.3.1 through2: 4.0.2 + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.3 + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -7571,8 +8098,22 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + lodash.merge@4.6.2: {} + lodash.once@4.1.1: {} + long@5.3.2: {} loupe@3.2.1: {} @@ -7701,6 +8242,17 @@ snapshots: ms@2.1.3: {} + mssql@12.2.0(@azure/core-client@1.10.1): + dependencies: + '@tediousjs/connection-string': 0.6.0 + commander: 11.1.0 + debug: 4.4.3 + tarn: 3.0.2 + tedious: 19.2.1(@azure/core-client@1.10.1) + transitivePeerDependencies: + - '@azure/core-client' + - supports-color + multipasta@0.2.5: {} mysql2@3.16.3: @@ -7723,6 +8275,8 @@ snapshots: napi-build-utils@2.0.0: {} + native-duplexpair@1.0.0: {} + natural-compare@1.4.0: {} negotiator@0.6.3: {} @@ -7792,6 +8346,13 @@ snapshots: dependencies: mimic-fn: 2.1.0 + open@10.2.0: + dependencies: + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + wsl-utils: 0.1.0 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -7964,6 +8525,8 @@ snapshots: process-nextick-args@2.0.1: {} + process@0.11.10: {} + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -8038,6 +8601,14 @@ snapshots: string_decoder: 1.1.1 util-deprecate: 1.0.2 + readable-stream@4.7.0: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -8119,6 +8690,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.53.5 fsevents: 2.3.3 + run-applescript@7.1.0: {} + rxjs@7.8.2: dependencies: tslib: 2.7.0 @@ -8363,6 +8936,8 @@ snapshots: sprintf-js@1.0.3: {} + sprintf-js@1.1.3: {} + sqlstring@2.3.3: {} stackback@0.0.2: {} @@ -8429,6 +9004,10 @@ snapshots: dependencies: safe-buffer: 5.1.2 + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -8491,6 +9070,24 @@ snapshots: mkdirp: 3.0.1 yallist: 5.0.0 + tarn@3.0.2: {} + + tedious@19.2.1(@azure/core-client@1.10.1): + dependencies: + '@azure/core-auth': 1.10.1 + '@azure/identity': 4.13.0 + '@azure/keyvault-keys': 4.10.0(@azure/core-client@1.10.1) + '@js-joda/core': 5.7.0 + '@types/node': 24.10.4 + bl: 6.1.6 + iconv-lite: 0.7.2 + js-md4: 0.3.2 + native-duplexpair: 1.0.0 + sprintf-js: 1.1.3 + transitivePeerDependencies: + - '@azure/core-client' + - supports-color + text-decoder@1.2.1: {} through2@2.0.5: @@ -8549,6 +9146,8 @@ snapshots: tslib@2.7.0: {} + tslib@2.8.1: {} + tsx@4.21.0: dependencies: esbuild: 0.27.1 @@ -8668,6 +9267,8 @@ snapshots: utils-merge@1.0.1: {} + uuid@8.3.2: {} + vary@1.1.2: {} vite-node@3.2.4(@types/node@24.10.4)(tsx@4.21.0)(yaml@2.8.2): @@ -8883,6 +9484,10 @@ snapshots: ws@8.18.3: {} + wsl-utils@0.1.0: + dependencies: + is-wsl: 3.1.1 + xtend@4.0.2: {} y18n@5.0.8: {}