Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JsonPath arrow & dot improvements #2

Merged
merged 2 commits into from
Oct 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@a2lix/schemql",
"version": "0.1.1",
"version": "0.2.0",
"description": "A lightweight TypeScript library that enhances your SQL workflow by combining raw SQL with targeted type safety and schema validation",
"license": "MIT",
"keywords": [
Expand All @@ -17,7 +17,9 @@
"dbms-agnostic",
"lightweight",
"sql-first",
"query-builder"
"query-builder",
"json",
"jsonpath"
],
"author": {
"name": "David ALLIX",
Expand Down Expand Up @@ -56,8 +58,8 @@
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@types/node": "^22.7.6",
"pkgroll": "^2.5.0",
"@types/node": "^22.8.1",
"pkgroll": "^2.5.1",
"ts-node": "^10.9.2",
"tsx": "^4.19.1",
"typescript": "^5.6.3"
Expand Down
54 changes: 27 additions & 27 deletions pnpm-lock.yaml

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

63 changes: 53 additions & 10 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,35 @@ type ValidTableColumnCombinations<DB> = {
[T in TableNames<DB>]: `${T}.${ColumnNames<DB, T>}` | `${T}.${ColumnNames<DB, T>}-`
}[TableNames<DB>]

type JsonPathForObject<T, Path extends string = ''> = {
[K in keyof T & string]:
| `${Path}->'${K}'`
| `${Path}->'${K}'-`
| `${Path}->>'${K}'`
| `${Path}->>'${K}'-`
| (T[K] extends object ? JsonPathForObject<T[K], `${Path}->'${K}'`> : never)
}[keyof T & string]
type JsonPathForObjectArrow<T, P extends string = ''> = T extends Record<string, any>
? {
[K in keyof T & string]:
| `${P}->${K}`
| `${P}->${K}-`
| `${P}->>${K}`
| `${P}->>${K}-`
| (NonNullable<T[K]> extends Record<string, any>
? `${P}->${K}${JsonPathForObjectArrow<NonNullable<T[K]>, ''>}`
: never)
}[keyof T & string]
: ''

type JsonPathForObjectDot<T, P extends string = ''> = T extends Record<string, any>
? {
[K in keyof T & string]:
| `${P}.${K}`
| (NonNullable<T[K]> extends Record<string, any>
? `${P}.${K}${JsonPathForObjectDot<NonNullable<T[K]>, ''>}`
: never)
}[keyof T & string]
: ''

type JsonPathCombinations<DB, T extends TableNames<DB>> = {
[K in ColumnNames<DB, T>]: DB[T][K] extends object ? JsonPathForObject<DB[T][K], `${T}.${K}`> : never
[K in ColumnNames<DB, T>]: DB[T][K] extends object
?
| JsonPathForObjectArrow<ArrayElement<DB[T][K]>, `${T}.${K} `>
| JsonPathForObjectDot<ArrayElement<DB[T][K]>, `${T}.${K} $`>
: never
}[ColumnNames<DB, T>]

type ValidJsonPathCombinations<DB> = {
Expand Down Expand Up @@ -163,7 +181,32 @@ export class SchemQl<DB> {
if (typeof value === 'string') {
switch (true) {
case value.startsWith('@'): {
return value.endsWith('-') ? (value.split('.')[1]?.slice(0, -1) ?? '') : value.slice(1)
// JsonPath dot? Add quotes
const jsonPathDotIndex = value.indexOf(' $.')
if (jsonPathDotIndex !== -1) {
return `'${value.slice(jsonPathDotIndex + 1)}'`
}

let str: string = value
// JsonPath arrow? Add quotes
const jsonPathArrowIndex = str.indexOf(' ->')
if (jsonPathArrowIndex !== -1) {
const jsonPathArrow = str
.slice(jsonPathArrowIndex + 1)
.split(/(?=->)/)
.reduce((path, segment) => {
const arrow = segment.startsWith('->>') ? '->>' : '->'
const value = segment.replace(arrow, '')
return `${path}${arrow}'${value}'`
}, '')
str = `${str.slice(0, jsonPathArrowIndex)}${jsonPathArrow}`
}

if (str.endsWith('-')) {
return str.split('.')[1]?.slice(0, -1) ?? ''
}

return str.slice(1)
}
case value.startsWith('$'):
case value.startsWith('§'): // Trick for cond/raw
Expand Down
69 changes: 65 additions & 4 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,10 +251,10 @@ describe('SchemQl - sql literal', () => {
normalizeString(`
SELECT
*,
LENGTH(id) AS length_id
LENGTH(users.id) AS length_id
FROM users
WHERE
id = :id
users.id = :id
`)
)
assert.deepEqual(params, { id: 'uuid-1' })
Expand All @@ -269,10 +269,10 @@ describe('SchemQl - sql literal', () => {
normalizeString(s.sql`
SELECT
*,
LENGTH(${'@users.id-'}) AS ${'$length_id'}
LENGTH(${'@users.id'}) AS ${'$length_id'}
FROM ${'@users'}
WHERE
${'@users.id-'} = ${':id'}
${'@users.id'} = ${':id'}
`)
)

Expand Down Expand Up @@ -338,6 +338,67 @@ describe('SchemQl - sql literal advanced', () => {
disabled_at: null,
})
})

it('should return the expected result - with sql helper', async () => {
const result = await schemQlUnconfigured.first({
queryFn: (sql, params) => {
assert.strictEqual(
sql,
normalizeString(`
UPDATE users
SET
metadata = json_set(users.metadata,
'$.email_variant', :emailVariant,
'$.email_verified_at', :emailVerifiedAt
)
WHERE
users.metadata->'role' = :role
RETURNING *
`)
)
assert.deepEqual(params, { role: 'admin', emailVariant: '[email protected]', emailVerifiedAt: 1500000000 })
return {
id: 'uuid-2',
email: '[email protected]',
metadata: '{"role":"admin","email_variant":"[email protected]","email_verified_at":1500000000}',
created_at: 1500000000,
disabled_at: null,
}
},
resultSchema: zUserDb,
params: {
role: 'admin',
emailVariant: '[email protected]',
emailVerifiedAt: 1500000000,
},
paramsSchema: z.object({ role: z.string(), emailVariant: z.string(), emailVerifiedAt: z.number().int() }),
})((s) =>
normalizeString(s.sql`
UPDATE ${'@users'}
SET
${'@users.metadata-'} = json_set(${'@users.metadata'},
${'@users.metadata $.email_variant'}, ${':emailVariant'},
${'@users.metadata $.email_verified_at'}, ${':emailVerifiedAt'}
)
WHERE
${'@users.metadata ->role'} = ${':role'}
RETURNING *
`)
)

assert.deepEqual(result, {
id: 'uuid-2',
email: '[email protected]',
metadata: {
role: 'admin',
email_variant: '[email protected]',
email_verified_at: 1500000000,
},
created_at: 1500000000,
disabled_at: null,
})
})

it('should return the expected result - with sqlCond & sqlRaw helpers', async () => {
const result = await schemQlUnconfigured.all({
queryFn: (sql, params) => {
Expand Down
2 changes: 2 additions & 0 deletions tests/schema_zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export const zUserDb = z.object({
parseJsonPreprocessor,
z.object({
role: z.enum(['user', 'admin']).default('user'),
email_variant: z.string().optional(),
email_verified_at: z.number().int().optional(),
})
),
created_at: z.number().int(),
Expand Down