Skip to content

Commit

Permalink
Merge pull request #5 from johngeorgewright/pql
Browse files Browse the repository at this point in the history
feat: add pql function
  • Loading branch information
johngeorgewright authored Sep 20, 2023
2 parents 7270e72 + 9258343 commit a45ac9a
Show file tree
Hide file tree
Showing 9 changed files with 304 additions and 26 deletions.
24 changes: 14 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ npm install @johngw/google-ad-manager-api
## Usage

```typescript
import { v202308 } from '../src'
import { v202308, pql, not, like } from '@johngw/google-ad-manager-api'

const api = new v202308({
const api = new v202308.GoogleAdManager({
applicationName: 'MY_APPLICATION_NAME',
networkCode: 123456789,
jwtOptions: {
Expand All @@ -23,15 +23,19 @@ const api = new v202308({
},
})

const [GetLineItemsByStatementResponse] = await api
.createLineItemServiceClient()
.then((client) =>
client.getLineItemsByStatementAsync({
filterStatement: {
query: 'LIMIT 10',
const client = await api.createLineItemServiceClient()

const [response] = await client.getLineItemsByStatementAsync({
filterStatement: {
query: pql<v202308.LineItemService.LineItems>({
limit: 10,
where: {
id: not(11222),
name: like('foo %'),
},
}),
)
},
})

expect(GetLineItemsByStatementResponse.rval?.results).toHaveLength(10)
expect(response.rval?.results).toHaveLength(10)
```
27 changes: 17 additions & 10 deletions src/build/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@ import { BearerSecurity, Client, createClientAsync } from 'soap'
${mapJoin(
services,
(service) =>
`import { createClientAsync as create${service}Client } from '../service/${version}/${service.toLowerCase()}'`,
/*ts*/ `import { createClientAsync as create${service}Client } from '../service/${version}/${service.toLowerCase()}'`,
)}
${mapJoin(
services,
(service) =>
/*ts*/ `export * as ${service} from '../service/${version}/${service.toLowerCase()}'`,
)}
export interface GoogleAdManagerOptions {
Expand Down Expand Up @@ -70,7 +76,7 @@ export class GoogleAdManager {
) => Promise<C>,
wsdlPath: string
) {
return async () => {
return async (): Promise<C> => {
const [token, client] = await Promise.all([
this.#jwt.authorize(),
createClient(wsdlPath, {
Expand Down Expand Up @@ -101,14 +107,15 @@ async function generateIndex() {
const filePath = 'src/index.ts'
await writeFile(
filePath,
mapJoin(
apis,
(api) =>
`export { GoogleAdManager as ${basename(
api,
extname(api),
)} } from './api/${basename(api, extname(api))}'`,
),
/* ts */ `export * from './query/pql'
${mapJoin(
apis,
(api) =>
/* ts */ `export * as ${basename(api, extname(api))} from './api/${basename(
api,
extname(api),
)}'`,
)}`,
)
}

Expand Down
5 changes: 5 additions & 0 deletions src/lang/Array.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export function castArray<T>(x: undefined | T | T[]): T[] {
if (x === undefined) return []
if (Array.isArray(x)) return x
return [x]
}
3 changes: 3 additions & 0 deletions src/lang/Object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function entries<X extends Object>(x: X) {
return Object.entries(x) as [keyof X, X[keyof X]][]
}
118 changes: 118 additions & 0 deletions src/query/pql.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { PickOfValue } from '../types/PickOfValue'
import { entries } from '../lang/Object'

type Contionable = string | boolean | number

type Condition<T> = {
[K in keyof PickOfValue<T, Contionable>]?:
| Is<Exclude<T[K], undefined>>
| IsNot<
Exclude<T[K], undefined> extends string
? Exclude<T[K], undefined> | IsLike
: Exclude<T[K], undefined>
>
| IsLike
}

type Is<T> = null | (T extends boolean ? T : T | T[])

interface IsNot<T> {
__type__: 'not'
value: Is<T>
}

type IsLikeString = `${string}%${string}`

interface IsLike {
__type__: 'like'
value: IsLikeString
}

export function not<T>(value: Is<T>): IsNot<T> {
return { __type__: 'not', value }
}

export function like(value: IsLikeString): IsLike {
return { __type__: 'like', value }
}

export function pql<T>({
limit,
offset,
where: conditions,
}: {
limit?: number
offset?: number
where?: Condition<T> | Condition<T>[]
}): string {
let pql = ''

if (conditions) {
pql += `WHERE ${
Array.isArray(conditions)
? `(${conditions.map(where).join(') OR (')})`
: where(conditions)
}`
}

if (limit) {
pql += ` LIMIT ${limit}`
}

if (offset) {
pql += ` OFFSET ${offset}`
}

return pql

function where(condition: Condition<T>) {
return entries(condition)
.map(([key, value]) => formatCondition(key, value))
.join(' AND ')
}
}

function formatCondition(key: keyof any, value: unknown) {
const not = isNot(value)
const x = not ? value.value : value

return Array.isArray(x)
? `${not ? 'NOT ' : ''}${key.toString()} IN (${x.map(formatValue)})`
: isLike(x)
? `${not ? 'NOT ' : ''}${key.toString()} LIKE ${formatValue(x)}`
: `${key.toString()} ${not ? '!=' : '='} ${formatValue(x)}`
}

function formatValue(value: Is<any>): string {
switch (typeof value) {
case 'boolean':
return value ? 'TRUE' : 'FALSE'
case 'number':
return value.toString()
default:
return isNot(value) || isLike(value)
? formatValue(value.value)
: `'${value.replace("'", "\\'")}'`
}
}

function isNot<T>(x: unknown): x is IsNot<T> {
return (
typeof x === 'object' &&
x !== null &&
'__type__' in x &&
x.__type__ === 'not' &&
'value' in x
)
}

function isLike(x: unknown): x is IsLike {
return (
typeof x === 'object' &&
x !== null &&
'__type__' in x &&
x.__type__ === 'like' &&
'value' in x &&
typeof x.value === 'string'
)
}
3 changes: 3 additions & 0 deletions src/types/PickOfValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type PickOfValue<T, V> = {
[K in keyof T as Extract<T[K], V> extends never ? never : K]: T[K]
}
13 changes: 13 additions & 0 deletions src/types/ResultItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export type ResultItem<R extends Response<unknown>> = R extends Response<
infer T
>
? T
: never

interface Response<T> {
rval?: Rval<T>
}

interface Rval<T> {
results?: T[]
}
125 changes: 125 additions & 0 deletions test/pql.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { like, not, pql } from '../src/query/pql'
import { Creatives } from '../src/service/v202308/creativeservice'
import { LineItems } from '../src/service/v202308/lineitemservice'

test('positive', () => {
expect(
pql<Creatives>({
where: {
name: 'foo',
},
}),
).toBe("WHERE name = 'foo'")

expect(
pql<Creatives>({
where: {
name: "foo's",
},
}),
).toBe("WHERE name = 'foo\\'s'")

expect(
pql<Creatives>({
where: {
id: 1234,
},
}),
).toBe('WHERE id = 1234')

expect(
pql<LineItems>({
where: {
allowOverbook: true,
},
}),
).toBe('WHERE allowOverbook = TRUE')

expect(
pql<LineItems>({
where: {
allowOverbook: false,
},
}),
).toBe('WHERE allowOverbook = FALSE')

expect(
pql<Creatives>({
where: {
name: ['foo', 'bar'],
},
}),
).toBe("WHERE name IN ('foo','bar')")

expect(
pql<Creatives>({
where: {
name: like('foo %'),
},
}),
).toBe("WHERE name LIKE 'foo %'")
})

test('negative', () => {
expect(
pql<Creatives>({
where: {
name: not('foo'),
},
}),
).toBe("WHERE name != 'foo'")

expect(
pql<Creatives>({
where: {
id: not(1234),
},
}),
).toBe('WHERE id != 1234')

expect(
pql<Creatives>({
where: {
name: not(['foo', 'bar']),
},
}),
).toBe("WHERE NOT name IN ('foo','bar')")

expect(
pql<Creatives>({
where: {
name: not(like('foo %')),
},
}),
).toBe("WHERE NOT name LIKE 'foo %'")
})

test('ands', () => {
expect(
pql<Creatives>({
where: {
name: 'foo',
previewUrl: 'bar',
},
}),
).toBe("WHERE name = 'foo' AND previewUrl = 'bar'")
})

test('ors', () => {
expect(
pql<Creatives>({
where: [
{
name: 'foo',
previewUrl: 'bar',
},
{
id: 123,
advertiserId: 333,
},
],
}),
).toBe(
"WHERE (name = 'foo' AND previewUrl = 'bar') OR (id = 123 AND advertiserId = 333)",
)
})
12 changes: 6 additions & 6 deletions test/v202208.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { v202308 as GoogleAdManager } from '../src'
import { v202308, pql } from '../src'
import { load as dotenv } from 'dotenv-extended'

beforeAll(() =>
Expand All @@ -8,10 +8,10 @@ beforeAll(() =>
}),
)

let api: GoogleAdManager
let api: v202308.GoogleAdManager

beforeEach(() => {
api = new GoogleAdManager({
api = new v202308.GoogleAdManager({
applicationName: 'google-ad-manager-api',
networkCode: Number(process.env.NETWORK_CODE),
jwtOptions: {
Expand All @@ -25,12 +25,12 @@ beforeEach(() => {
test('line items', async () => {
const lineItemServerClient = await api.createLineItemServiceClient()

const [GetLineItemsByStatementResponse] =
const [getLineItemsByStatementResponse] =
await lineItemServerClient.getLineItemsByStatementAsync({
filterStatement: {
query: 'LIMIT 10',
query: pql<v202308.LineItemService.LineItems>({ limit: 10 }),
},
})

expect(GetLineItemsByStatementResponse.rval?.results).toHaveLength(10)
expect(getLineItemsByStatementResponse.rval?.results).toHaveLength(10)
})

0 comments on commit a45ac9a

Please sign in to comment.