🙊 escape sql strings

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-03-28 15:57:13 +01:00
parent 40b98f36a4
commit ecf68463c9
16 changed files with 104 additions and 56 deletions

View File

@@ -24,6 +24,7 @@
"pino-pretty": "^10.3.1",
"ramda": "^0.29.1",
"sharp": "^0.33.2",
"sqlstring": "^2.3.3",
"ua-parser-js": "^1.0.37",
"url-metadata": "^4.1.0",
"uuid": "^9.0.1"
@@ -34,6 +35,7 @@
"@openpanel/sdk": "workspace:*",
"@openpanel/tsconfig": "workspace:*",
"@types/ramda": "^0.29.6",
"@types/sqlstring": "^2.3.2",
"@types/ua-parser-js": "^0.7.39",
"@types/uuid": "^9.0.8",
"@types/ws": "^8.5.10",

View File

@@ -5,6 +5,7 @@ import { isUserAgentSet, parseUserAgent } from '@/utils/parseUserAgent';
import { isSameDomain, parsePath } from '@/utils/url';
import type { FastifyReply, FastifyRequest } from 'fastify';
import { omit } from 'ramda';
import { escape } from 'sqlstring';
import { v4 as uuid } from 'uuid';
import { generateDeviceId, getTime, toISOString } from '@openpanel/common';
@@ -103,7 +104,7 @@ export async function postEvent(
const [event] = await withTiming(
'Get last event (server-event)',
getEvents(
`SELECT * FROM events WHERE name = 'screen_view' AND profile_id = '${profileId}' AND project_id = '${projectId}' ORDER BY created_at DESC LIMIT 1`
`SELECT * FROM events WHERE name = 'screen_view' AND profile_id = ${escape(profileId)} AND project_id = ${escape(projectId)} ORDER BY created_at DESC LIMIT 1`
)
);
@@ -212,7 +213,7 @@ export async function postEvent(
'Get session start event',
Promise.all([
getEvents(
`SELECT * FROM events WHERE name = 'session_start' AND device_id = '${deviceId}' AND project_id = '${projectId}' ORDER BY created_at DESC LIMIT 1`
`SELECT * FROM events WHERE name = 'session_start' AND device_id = ${escape(deviceId)} AND project_id = ${escape(projectId)} ORDER BY created_at DESC LIMIT 1`
),
findJobByPrefix(eventsQueue, `event:${projectId}:${deviceId}:`),
])

View File

@@ -1,4 +1,5 @@
import type { FastifyReply, FastifyRequest } from 'fastify';
import { escape } from 'sqlstring';
import type * as WebSocket from 'ws';
import { getSafeJson } from '@openpanel/common';
@@ -19,7 +20,7 @@ export async function test(
reply: FastifyReply
) {
const [event] = await getEvents(
`SELECT * FROM events WHERE project_id = '${req.params.projectId}' AND name = 'screen_view' LIMIT 1`
`SELECT * FROM events WHERE project_id = ${escape(req.params.projectId)} AND name = 'screen_view' LIMIT 1`
);
if (!event) {
return reply.status(404).send('No event found');

View File

@@ -90,6 +90,7 @@
"short-unique-id": "^5.0.3",
"slugify": "^1.6.6",
"sonner": "^1.4.0",
"sqlstring": "^2.3.3",
"superjson": "^1.13.3",
"tailwind-merge": "^1.14.0",
"tailwindcss-animate": "^1.0.7",
@@ -109,6 +110,7 @@
"@types/react-dom": "^18.2.7",
"@types/react-syntax-highlighter": "^15.5.11",
"@types/request-ip": "^0.0.41",
"@types/sqlstring": "^2.3.2",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"autoprefixer": "^10.4.17",

View File

@@ -1,4 +1,5 @@
import { Widget } from '@/components/widget';
import { escape } from 'sqlstring';
import { db, getEvents } from '@openpanel/db';
@@ -21,7 +22,7 @@ export default async function EventConversionsListServer({ projectId }: Props) {
}
const events = await getEvents(
`SELECT * FROM events WHERE project_id = '${projectId}' AND name IN (${conversions.map((c) => `'${c.name}'`).join(', ')}) ORDER BY created_at DESC LIMIT 20;`,
`SELECT * FROM events WHERE project_id = ${escape(projectId)} AND name IN (${conversions.map((c) => escape(c.name)).join(', ')}) ORDER BY created_at DESC LIMIT 20;`,
{
profile: true,
meta: true,

View File

@@ -5,6 +5,7 @@ import {
} from '@/components/ui/tooltip';
import { Widget, WidgetBody, WidgetHead } from '@/components/widget';
import { cn } from '@/utils/cn';
import { escape } from 'sqlstring';
import { chQuery } from '@openpanel/db';
@@ -20,7 +21,7 @@ export default async function ProfileLastSeenServer({ projectId }: Props) {
// Days since last event from users
// group by days
const res = await chQuery<Row>(
`SELECT age('days',created_at, now()) as days, count(distinct profile_id) as count FROM events where project_id = '${projectId}' group by days order by days ASC`
`SELECT age('days',created_at, now()) as days, count(distinct profile_id) as count FROM events where project_id = ${escape(projectId)} group by days order by days ASC`
);
const take = 18;

View File

@@ -4,6 +4,7 @@ import { Widget, WidgetHead } from '@/components/widget';
import { WidgetTable } from '@/components/widget-table';
import { getProfileName } from '@/utils/getters';
import Link from 'next/link';
import { escape } from 'sqlstring';
import { chQuery, getProfiles } from '@openpanel/db';
@@ -19,7 +20,7 @@ export default async function ProfileTopServer({
// Days since last event from users
// group by days
const res = await chQuery<{ profile_id: string; count: number }>(
`SELECT profile_id, count(*) as count from events where profile_id != '' and project_id = '${projectId}' group by profile_id order by count() DESC LIMIT 10`
`SELECT profile_id, count(*) as count from events where profile_id != '' and project_id = ${escape(projectId)} group by profile_id order by count() DESC LIMIT 10`
);
const profiles = await getProfiles({ ids: res.map((r) => r.profile_id) });
const list = res.map((item) => {

View File

@@ -1,5 +1,6 @@
import { shortNumber } from '@/hooks/useNumerFormatter';
import Link from 'next/link';
import { escape } from 'sqlstring';
import type { IServiceProject } from '@openpanel/db';
import { chQuery } from '@openpanel/db';
@@ -13,19 +14,19 @@ export async function ProjectCard({
}: IServiceProject) {
const [chart, [data]] = await Promise.all([
chQuery<{ value: number; date: string }>(
`SELECT countDistinct(profile_id) as value, toStartOfDay(created_at) as date FROM events WHERE project_id = '${id}' AND name = 'session_start' AND created_at >= now() - interval '1 month' GROUP BY date ORDER BY date ASC`
`SELECT countDistinct(profile_id) as value, toStartOfDay(created_at) as date FROM events WHERE project_id = ${escape(id)} AND name = 'session_start' AND created_at >= now() - interval '1 month' GROUP BY date ORDER BY date ASC`
),
chQuery<{ total: number; month: number; day: number }>(
`
SELECT
(
SELECT count(DISTINCT profile_id) as count FROM events WHERE project_id = '${id}'
SELECT count(DISTINCT profile_id) as count FROM events WHERE project_id = ${escape(id)}
) as total,
(
SELECT count(DISTINCT profile_id) as count FROM events WHERE project_id = '${id}' AND created_at >= now() - interval '1 month'
SELECT count(DISTINCT profile_id) as count FROM events WHERE project_id = ${escape(id)} AND created_at >= now() - interval '1 month'
) as month,
(
SELECT count(DISTINCT profile_id) as count FROM events WHERE project_id = '${id}' AND created_at >= now() - interval '1 day'
SELECT count(DISTINCT profile_id) as count FROM events WHERE project_id = ${escape(id)} AND created_at >= now() - interval '1 day'
) as day
`
),

View File

@@ -2,6 +2,7 @@ import { round } from '@/utils/math';
import { subDays } from 'date-fns';
import * as mathjs from 'mathjs';
import { repeat, reverse, sort } from 'ramda';
import { escape } from 'sqlstring';
import { alphabetIds, NOT_SET_VALUE } from '@openpanel/constants';
import {
@@ -430,7 +431,7 @@ export async function getFunnelData({ projectId, ...payload }: IChartInput) {
const funnels = payload.events.map((event) => {
const { sb, getWhere } = createSqlBuilder();
sb.where = getEventFiltersWhereClause(event.filters);
sb.where.name = `name = '${event.name}'`;
sb.where.name = `name = ${escape(event.name)}`;
return getWhere().replace('WHERE ', '');
});
@@ -438,7 +439,7 @@ export async function getFunnelData({ projectId, ...payload }: IChartInput) {
session_id,
windowFunnel(6048000000000000,'strict_increase')(toUnixTimestamp(created_at), ${funnels.join(', ')}) AS level
FROM events
WHERE (project_id = '${projectId}' AND created_at >= '${formatClickhouseDate(startDate)}') AND (created_at <= '${formatClickhouseDate(endDate)}')
WHERE (project_id = ${escape(projectId)} AND created_at >= '${formatClickhouseDate(startDate)}') AND (created_at <= '${formatClickhouseDate(endDate)}')
GROUP BY session_id`;
const sql = `SELECT level, count() AS count FROM (${innerSql}) GROUP BY level ORDER BY level DESC`;
@@ -446,7 +447,7 @@ export async function getFunnelData({ projectId, ...payload }: IChartInput) {
const [funnelRes, sessionRes] = await Promise.all([
chQuery<{ level: number; count: number }>(sql),
chQuery<{ count: number }>(
`SELECT count(name) as count FROM events WHERE project_id = '${projectId}' AND name = 'session_start' AND (created_at >= '${formatClickhouseDate(startDate)}') AND (created_at <= '${formatClickhouseDate(endDate)}')`
`SELECT count(name) as count FROM events WHERE project_id = ${escape(projectId)} AND name = 'session_start' AND (created_at >= '${formatClickhouseDate(startDate)}') AND (created_at <= '${formatClickhouseDate(endDate)}')`
),
]);

View File

@@ -5,6 +5,7 @@ import {
} from '@/trpc/api/trpc';
import { average, max, min, round, sum } from '@/utils/math';
import { flatten, map, pipe, prop, sort, uniq } from 'ramda';
import { escape } from 'sqlstring';
import { z } from 'zod';
import { chQuery, createSqlBuilder } from '@openpanel/db';
@@ -60,7 +61,7 @@ export const chartRouter = createTRPCRouter({
.input(z.object({ projectId: z.string() }))
.query(async ({ input: { projectId } }) => {
const events = await chQuery<{ name: string }>(
`SELECT DISTINCT name FROM events WHERE project_id = '${projectId}'`
`SELECT DISTINCT name FROM events WHERE project_id = ${escape(projectId)}`
);
return [
@@ -76,8 +77,8 @@ export const chartRouter = createTRPCRouter({
.query(async ({ input: { projectId, event } }) => {
const events = await chQuery<{ keys: string[] }>(
`SELECT distinct mapKeys(properties) as keys from events where ${
event && event !== '*' ? `name = '${event}' AND ` : ''
} project_id = '${projectId}';`
event && event !== '*' ? `name = ${escape(event)} AND ` : ''
} project_id = ${escape(projectId)};`
);
const properties = events
@@ -122,14 +123,14 @@ export const chartRouter = createTRPCRouter({
)
.query(async ({ input: { event, property, projectId } }) => {
const { sb, getSql } = createSqlBuilder();
sb.where.project_id = `project_id = '${projectId}'`;
sb.where.project_id = `project_id = ${escape(projectId)}`;
if (event !== '*') {
sb.where.event = `name = '${event}'`;
sb.where.event = `name = ${escape(event)}`;
}
if (property.startsWith('properties.')) {
sb.select.values = `distinct mapValues(mapExtractKeyLike(properties, '${property
.replace(/^properties\./, '')
.replace('.*.', '.%.')}')) as values`;
sb.select.values = `distinct mapValues(mapExtractKeyLike(properties, ${escape(
property.replace(/^properties\./, '').replace('.*.', '.%.')
)})) as values`;
} else {
sb.select.values = `distinct ${property} as values`;
}

View File

@@ -4,6 +4,7 @@ import {
publicProcedure,
} from '@/trpc/api/trpc';
import { flatten, map, pipe, prop, sort, uniq } from 'ramda';
import { escape } from 'sqlstring';
import { z } from 'zod';
import { chQuery, createSqlBuilder } from '@openpanel/db';
@@ -13,7 +14,7 @@ export const profileRouter = createTRPCRouter({
.input(z.object({ projectId: z.string() }))
.query(async ({ input: { projectId } }) => {
const events = await chQuery<{ keys: string[] }>(
`SELECT distinct mapKeys(properties) as keys from profiles where project_id = '${projectId}';`
`SELECT distinct mapKeys(properties) as keys from profiles where project_id = ${escape(projectId)};`
);
const properties = events
@@ -40,11 +41,11 @@ export const profileRouter = createTRPCRouter({
.query(async ({ input: { property, projectId } }) => {
const { sb, getSql } = createSqlBuilder();
sb.from = 'profiles';
sb.where.project_id = `project_id = '${projectId}'`;
sb.where.project_id = `project_id = ${escape(projectId)}`;
if (property.startsWith('properties.')) {
sb.select.values = `distinct mapValues(mapExtractKeyLike(properties, '${property
.replace(/^properties\./, '')
.replace('.*.', '.%.')}')) as values`;
sb.select.values = `distinct mapValues(mapExtractKeyLike(properties, ${escape(
property.replace(/^properties\./, '').replace('.*.', '.%.')
)})) as values`;
} else {
sb.select.values = `${property} as values`;
}

View File

@@ -20,6 +20,7 @@
"@openpanel/validation": "workspace:*",
"@prisma/client": "^5.1.1",
"ramda": "^0.29.1",
"sqlstring": "^2.3.3",
"uuid": "^9.0.1"
},
"devDependencies": {
@@ -28,6 +29,7 @@
"@openpanel/tsconfig": "workspace:*",
"@types/node": "^18.16.0",
"@types/ramda": "^0.29.6",
"@types/sqlstring": "^2.3.2",
"@types/uuid": "^9.0.8",
"eslint": "^8.48.0",
"prettier": "^3.0.3",

View File

@@ -1,10 +1,11 @@
import { escape } from 'sqlstring';
import type {
IChartEventFilter,
IGetChartDataInput,
} from '@openpanel/validation';
import { formatClickhouseDate } from '../clickhouse-client';
import type { SqlBuilderObject } from '../sql-builder';
import { createSqlBuilder } from '../sql-builder';
function log(sql: string) {
@@ -25,10 +26,10 @@ export function getChartSql({
createSqlBuilder();
sb.where = getEventFiltersWhereClause(event.filters);
sb.where.projectId = `project_id = '${projectId}'`;
sb.where.projectId = `project_id = ${escape(projectId)}`;
if (event.name !== '*') {
sb.select.label = `'${event.name}' as label`;
sb.where.eventName = `name = '${event.name}'`;
sb.select.label = `${escape(event.name)} as label`;
sb.where.eventName = `name = ${escape(event.name)}`;
}
sb.select.count = `count(*) as count`;
@@ -64,10 +65,10 @@ export function getChartSql({
const breakdown = breakdowns[0]!;
if (breakdown) {
const value = breakdown.name.startsWith('properties.')
? `mapValues(mapExtractKeyLike(properties, '${breakdown.name
.replace(/^properties\./, '')
.replace('.*.', '.%.')}'))`
: breakdown.name;
? `mapValues(mapExtractKeyLike(properties, ${escape(
breakdown.name.replace(/^properties\./, '').replace('.*.', '.%.')
)}))`
: escape(breakdown.name);
sb.select.label = breakdown.name.startsWith('properties.')
? `arrayElement(${value}, 1) as label`
: `${breakdown.name} as label`;
@@ -120,32 +121,32 @@ export function getEventFiltersWhereClause(filters: IChartEventFilter[]) {
if (value.length === 0) return;
if (name.startsWith('properties.')) {
const whereFrom = `mapValues(mapExtractKeyLike(properties, '${name
.replace(/^properties\./, '')
.replace('.*.', '.%.')}'))`;
const whereFrom = `mapValues(mapExtractKeyLike(properties, ${escape(
name.replace(/^properties\./, '').replace('.*.', '.%.')
)}))`;
switch (operator) {
case 'is': {
where[id] = `arrayExists(x -> ${value
.map((val) => `x = '${String(val).trim()}'`)
.map((val) => `x = ${escape(String(val).trim())}`)
.join(' OR ')}, ${whereFrom})`;
break;
}
case 'isNot': {
where[id] = `arrayExists(x -> ${value
.map((val) => `x != '${String(val).trim()}'`)
.map((val) => `x != ${escape(String(val).trim())}`)
.join(' OR ')}, ${whereFrom})`;
break;
}
case 'contains': {
where[id] = `arrayExists(x -> ${value
.map((val) => `x LIKE '%${String(val).trim()}%'`)
.map((val) => `x LIKE ${escape(`%${String(val).trim()}%`)}`)
.join(' OR ')}, ${whereFrom})`;
break;
}
case 'doesNotContain': {
where[id] = `arrayExists(x -> ${value
.map((val) => `x NOT LIKE '%${String(val).trim()}%'`)
.map((val) => `x NOT LIKE ${escape(`%${String(val).trim()}%`)}`)
.join(' OR ')}, ${whereFrom})`;
break;
}
@@ -154,25 +155,27 @@ export function getEventFiltersWhereClause(filters: IChartEventFilter[]) {
switch (operator) {
case 'is': {
where[id] = `${name} IN (${value
.map((val) => `'${String(val).trim()}'`)
.map((val) => escape(String(val).trim()))
.join(', ')})`;
break;
}
case 'isNot': {
where[id] = `${name} NOT IN (${value
.map((val) => `'${String(val).trim()}'`)
.map((val) => escape(String(val).trim()))
.join(', ')})`;
break;
}
case 'contains': {
where[id] = value
.map((val) => `${name} LIKE '%${String(val).trim()}%'`)
.map((val) => `${name} LIKE ${escape(`%${String(val).trim()}%`)}`)
.join(' OR ');
break;
}
case 'doesNotContain': {
where[id] = value
.map((val) => `${name} NOT LIKE '%${String(val).trim()}%'`)
.map(
(val) => `${name} NOT LIKE ${escape(`%${String(val).trim()}%`)}`
)
.join(' OR ');
break;
}

View File

@@ -1,4 +1,5 @@
import { omit, uniq } from 'ramda';
import { escape } from 'sqlstring';
import { v4 as uuid } from 'uuid';
import { randomSplitName, toDots } from '@openpanel/common';
@@ -261,15 +262,15 @@ export async function getEventList({
sb.limit = take;
sb.offset = Math.max(0, (cursor ?? 0) * take);
sb.where.projectId = `project_id = '${projectId}'`;
sb.where.projectId = `project_id = ${escape(projectId)}`;
if (profileId) {
sb.where.deviceId = `device_id IN (SELECT device_id as did FROM openpanel.events WHERE profile_id = '${profileId}' group by did)`;
sb.where.deviceId = `device_id IN (SELECT device_id as did FROM openpanel.events WHERE profile_id = ${escape(profileId)} group by did)`;
}
if (events && events.length > 0) {
sb.where.events = `name IN (${join(
events.map((n) => `'${n}'`),
events.map((event) => escape(event)),
','
)})`;
}
@@ -297,14 +298,14 @@ export async function getEventsCount({
filters,
}: Omit<GetEventListOptions, 'cursor' | 'take'>) {
const { sb, getSql, join } = createSqlBuilder();
sb.where.projectId = `project_id = '${projectId}'`;
sb.where.projectId = `project_id = ${escape(projectId)}`;
if (profileId) {
sb.where.profileId = `profile_id = '${profileId}'`;
sb.where.profileId = `profile_id = ${escape(profileId)}`;
}
if (events && events.length > 0) {
sb.where.events = `name IN (${join(
events.map((n) => `'${n}'`),
events.map((event) => escape(event)),
','
)})`;
}

View File

@@ -1,3 +1,5 @@
import { escape } from 'sqlstring';
import { toDots, toObject } from '@openpanel/common';
import type { IChartEventFilter } from '@openpanel/validation';
@@ -11,7 +13,7 @@ export async function getProfileById(id: string) {
}
const [profile] = await chQuery<IClickhouseProfile>(
`SELECT *, created_at as max_created_at FROM profiles WHERE id = '${id}' ORDER BY created_at DESC LIMIT 1`
`SELECT *, created_at as max_created_at FROM profiles WHERE id = ${escape(id)} ORDER BY created_at DESC LIMIT 1`
);
if (!profile) {
@@ -53,7 +55,7 @@ export async function getProfiles({ ids }: GetProfilesOptions) {
`SELECT
${getProfileSelectFields()}
FROM profiles
WHERE id IN (${ids.map((id) => `'${id}'`).join(',')})
WHERE id IN (${ids.map((id) => escape(id)).join(',')})
GROUP BY id
`
);
@@ -66,7 +68,7 @@ function getProfileInnerSelect(projectId: string) {
${getProfileSelectFields()}
FROM profiles
GROUP BY id
HAVING project_id = '${projectId}')`;
HAVING project_id = ${escape(projectId)})`;
}
export async function getProfileList({
@@ -120,7 +122,7 @@ export async function getProfilesByExternalId(
${getProfileSelectFields()}
FROM profiles
GROUP BY id
HAVING project_id = '${projectId}' AND external_id = '${externalId}'
HAVING project_id = ${escape(projectId)} AND external_id = ${escape(externalId)}
`
);
@@ -192,7 +194,7 @@ export async function upsertProfile({
projectId,
}: IServiceUpsertProfile) {
const [profile] = await chQuery<IClickhouseProfile>(
`SELECT * FROM profiles WHERE id = '${id}' AND project_id = '${projectId}' ORDER BY created_at DESC LIMIT 1`
`SELECT * FROM profiles WHERE id = ${escape(id)} AND project_id = ${escape(projectId)} ORDER BY created_at DESC LIMIT 1`
);
await ch.insert({

27
pnpm-lock.yaml generated
View File

@@ -65,6 +65,9 @@ importers:
sharp:
specifier: ^0.33.2
version: 0.33.2
sqlstring:
specifier: ^2.3.3
version: 2.3.3
ua-parser-js:
specifier: ^1.0.37
version: 1.0.37
@@ -90,6 +93,9 @@ importers:
'@types/ramda':
specifier: ^0.29.6
version: 0.29.10
'@types/sqlstring':
specifier: ^2.3.2
version: 2.3.2
'@types/ua-parser-js':
specifier: ^0.7.39
version: 0.7.39
@@ -345,6 +351,9 @@ importers:
sonner:
specifier: ^1.4.0
version: 1.4.0(react-dom@18.2.0)(react@18.2.0)
sqlstring:
specifier: ^2.3.3
version: 2.3.3
superjson:
specifier: ^1.13.3
version: 1.13.3
@@ -397,6 +406,9 @@ importers:
'@types/request-ip':
specifier: ^0.0.41
version: 0.0.41
'@types/sqlstring':
specifier: ^2.3.2
version: 2.3.2
'@typescript-eslint/eslint-plugin':
specifier: ^6.21.0
version: 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3)
@@ -776,6 +788,9 @@ importers:
ramda:
specifier: ^0.29.1
version: 0.29.1
sqlstring:
specifier: ^2.3.3
version: 2.3.3
uuid:
specifier: ^9.0.1
version: 9.0.1
@@ -795,6 +810,9 @@ importers:
'@types/ramda':
specifier: ^0.29.6
version: 0.29.10
'@types/sqlstring':
specifier: ^2.3.2
version: 2.3.2
'@types/uuid':
specifier: ^9.0.8
version: 9.0.8
@@ -7012,6 +7030,10 @@ packages:
'@types/mime': 3.0.4
'@types/node': 18.19.17
/@types/sqlstring@2.3.2:
resolution: {integrity: sha512-lVRe4Iz9UNgiHelKVo8QlC8fb5nfY8+p+jNQNE+UVsuuVlQnWhyWmQ/wF5pE8Ys6TdjfVpqTG9O9i2vi6E0+Sg==}
dev: true
/@types/stack-trace@0.0.29:
resolution: {integrity: sha512-TgfOX+mGY/NyNxJLIbDWrO9DjGoVSW9+aB8H2yy1fy32jsvxijhmyJI9fDFgvz3YP4lvJaq9DzdR/M1bOgVc9g==}
dev: false
@@ -15958,6 +15980,11 @@ packages:
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
dev: false
/sqlstring@2.3.3:
resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==}
engines: {node: '>= 0.6'}
dev: false
/ssri@8.0.1:
resolution: {integrity: sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==}
engines: {node: '>= 8'}