Skip to content

Commit

Permalink
Merge branch 'master' into ci-split-jobs
Browse files Browse the repository at this point in the history
  • Loading branch information
igalklebanov authored Apr 21, 2024
2 parents dee6c4d + b5e2fce commit 7d1d508
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 15 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion site/docs/getting-started/Instantiation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@ function getDialectSpecificCodeSnippet(
const driverNPMPackageName = getDriverNPMPackageNames(packageManager)[dialect]
const dialectClassName = DIALECT_CLASS_NAMES[dialect]
const poolClassName = 'Pool'
const poolClassImport = packageManager === 'deno' ? poolClassName : `{ ${poolClassName} }`

if (dialect === 'postgresql') {
return `import { ${poolClassName} } from '${driverNPMPackageName}'
return `import ${poolClassImport} from '${driverNPMPackageName}'
import { Kysely, ${dialectClassName} } from 'kysely'
const dialect = new ${dialectClassName}({
Expand Down
51 changes: 40 additions & 11 deletions src/plugin/parse-json-results/parse-json-results-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,22 @@ import {
PluginTransformResultArgs,
} from '../kysely-plugin.js'

export interface ParseJSONResultsPluginOptions {
/**
* When `'in-place'`, arrays' and objects' values are parsed in-place. This is
* the most time and space efficient option.
*
* This can result in runtime errors if some objects/arrays are readonly.
*
* When `'create'`, new arrays and objects are created to avoid such errors.
*
* Defaults to `'in-place'`.
*/
objectStrategy?: ObjectStrategy
}

type ObjectStrategy = 'in-place' | 'create'

/**
* Parses JSON strings in query results into JSON objects.
*
Expand All @@ -22,6 +38,12 @@ import {
* ```
*/
export class ParseJSONResultsPlugin implements KyselyPlugin {
readonly #objectStrategy: ObjectStrategy

constructor(readonly opt: ParseJSONResultsPluginOptions = {}) {
this.#objectStrategy = opt.objectStrategy || 'in-place'
}

// noop
transformQuery(args: PluginTransformQueryArgs): RootOperationNode {
return args.node
Expand All @@ -32,30 +54,32 @@ export class ParseJSONResultsPlugin implements KyselyPlugin {
): Promise<QueryResult<UnknownRow>> {
return {
...args.result,
rows: parseArray(args.result.rows),
rows: parseArray(args.result.rows, this.#objectStrategy),
}
}
}

function parseArray<T>(arr: T[]): T[] {
function parseArray<T>(arr: T[], objectStrategy: ObjectStrategy): T[] {
const target = objectStrategy === 'create' ? new Array(arr.length) : arr

for (let i = 0; i < arr.length; ++i) {
arr[i] = parse(arr[i]) as T
target[i] = parse(arr[i], objectStrategy) as T
}

return arr
return target
}

function parse(obj: unknown): unknown {
function parse(obj: unknown, objectStrategy: ObjectStrategy): unknown {
if (isString(obj)) {
return parseString(obj)
}

if (Array.isArray(obj)) {
return parseArray(obj)
return parseArray(obj, objectStrategy)
}

if (isPlainObject(obj)) {
return parseObject(obj)
return parseObject(obj, objectStrategy)
}

return obj
Expand All @@ -64,7 +88,7 @@ function parse(obj: unknown): unknown {
function parseString(str: string): unknown {
if (maybeJson(str)) {
try {
return parse(JSON.parse(str))
return parse(JSON.parse(str), 'in-place')
} catch (err) {
// this catch block is intentionally empty.
}
Expand All @@ -77,10 +101,15 @@ function maybeJson(value: string): boolean {
return value.match(/^[\[\{]/) != null
}

function parseObject(obj: Record<string, unknown>): Record<string, unknown> {
function parseObject(
obj: Record<string, unknown>,
objectStrategy: ObjectStrategy,
): Record<string, unknown> {
const target = objectStrategy === 'create' ? {} : obj

for (const key in obj) {
obj[key] = parse(obj[key])
target[key] = parse(obj[key], objectStrategy)
}

return obj
return target
}
2 changes: 1 addition & 1 deletion src/query-builder/merge-query-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -705,7 +705,7 @@ export class WheneableMergeQueryBuilder<

compile(): CompiledQuery<never> {
return this.#props.executor.compileQuery(
this.#props.queryNode,
this.toOperationNode(),
this.#props.queryId,
)
}
Expand Down
38 changes: 38 additions & 0 deletions test/node/src/camel-case.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
expect,
createTableWithId,
DIALECTS,
NOT_SUPPORTED,
} from './test-setup.js'

for (const dialect of DIALECTS) {
Expand Down Expand Up @@ -277,6 +278,43 @@ for (const dialect of DIALECTS) {
disable_emails: true,
})
})

if (dialect === 'postgres' || dialect === 'mssql') {
it('should convert merge queries', async () => {
const query = camelDb
.mergeInto('camelPerson')
.using(
'camelPerson as anotherCamelPerson',
'camelPerson.firstName',
'anotherCamelPerson.firstName',
)
.whenMatched()
.thenUpdateSet((eb) => ({
firstName: sql<string>`concat(${eb.ref('anotherCamelPerson.firstName')}, ${sql.lit('2')})`,
}))

testSql(query, dialect, {
postgres: {
sql: [
`merge into "camel_person"`,
`using "camel_person" as "another_camel_person" on "camel_person"."first_name" = "another_camel_person"."first_name"`,
`when matched then update set "first_name" = concat("another_camel_person"."first_name", '2')`,
],
parameters: [],
},
mysql: NOT_SUPPORTED,
mssql: {
sql: [
`merge into "camel_person"`,
`using "camel_person" as "another_camel_person" on "camel_person"."first_name" = "another_camel_person"."first_name"`,
`when matched then update set "first_name" = concat("another_camel_person"."first_name", '2');`,
],
parameters: [],
},
sqlite: NOT_SUPPORTED,
})
})
}
})
}

Expand Down
27 changes: 27 additions & 0 deletions test/node/src/parse-json-results-plugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { ParseJSONResultsPlugin } from '../../..'
import { createQueryId } from '../../../dist/cjs/util/query-id.js'

describe('ParseJSONResultsPlugin', () => {
describe("when `objectStrategy` is 'create'", () => {
let plugin: ParseJSONResultsPlugin

beforeEach(() => {
plugin = new ParseJSONResultsPlugin({ objectStrategy: 'create' })
})

it('should parse JSON results that contain readonly arrays/objects', async () => {
await plugin.transformResult({
queryId: createQueryId(),
result: {
rows: [
Object.freeze({
id: 1,
carIds: Object.freeze([1, 2, 3]),
metadata: JSON.stringify({ foo: 'bar' }),
}),
],
},
})
})
})
})

0 comments on commit 7d1d508

Please sign in to comment.