[WIP] - 🚧

This commit is contained in:
Zitao Xiong
2024-05-23 18:15:06 +08:00
parent a07d262277
commit 6a32320992
12 changed files with 2552 additions and 0 deletions

View File

@@ -9,7 +9,12 @@
"dependencies": {
"@mysten/bcs": "0.11.1",
"@mysten/sui.js": "0.54.1",
"@nestjs/common": "10.3.0",
"@nestjs/core": "10.3.0",
"@nestjs/microservices": "^10.3.0",
"@t3-oss/env-core": "^0.7.1",
"slonik": "^37.2.0",
"slonik-interceptor-query-logging": "^1.4.7",
"tslib": "^2.3.0",
"zod": "^3.22.4"
},
@@ -31,6 +36,7 @@
"@vitest/ui": "^1.3.1",
"eslint": "~8.57.0",
"eslint-config-prettier": "^9.0.0",
"npm-check-updates": "^16.14.20",
"nx": "19.0.6",
"prettier": "^2.6.2",
"tsx": "^4.10.3",

2315
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,41 @@
import { Logger } from '@nestjs/common';
import {
SchemaValidationError,
type Interceptor,
type QueryResultRow,
} from 'slonik';
const logger = new Logger('pg:interceptor:result-parser');
export const createResultParserInterceptor = (): Interceptor => {
return {
// If you are not going to transform results using Zod, then you should use `afterQueryExecution` instead.
// Future versions of Zod will provide a more efficient parser when parsing without transformations.
// You can even combine the two use `afterQueryExecution` to validate results, and (conditionally)
// transform results as needed in `transformRow`.
transformRow: (executionContext, actualQuery, row) => {
const { resultParser } = executionContext;
if (!resultParser) {
return row;
}
const validationResult = resultParser.safeParse(row);
if (!validationResult.success) {
logger.error(
`Validation result: ${JSON.stringify(validationResult, null, 2)}
query: ${actualQuery.sql}
value: ${JSON.stringify(actualQuery.values, null, 2)}
`,
);
throw new SchemaValidationError(
actualQuery,
row as QueryResultRow,
validationResult.error.issues,
);
}
return validationResult.data as QueryResultRow;
},
};
};

View File

@@ -0,0 +1,35 @@
import { TypeParser } from 'slonik';
const bigintParser = (value: string) => {
return BigInt(value);
};
// SELECT oid, typarray, typname FROM pg_type
export const createInt8TypeParser = (): TypeParser => {
return {
name: 'int8',
parse: bigintParser,
};
};
export const createFloat4TypeParser = (): TypeParser => {
return {
name: 'float4',
parse: bigintParser,
};
};
export const createFloat8TypeParser = (): TypeParser => {
return {
name: 'float8',
parse: bigintParser,
};
};
export const createIntegerTypeParser = (): TypeParser => {
return {
name: 'int4',
parse: bigintParser,
};
};

View File

@@ -0,0 +1,13 @@
import { TypeParser } from 'slonik';
import { hexToBuffer } from './utils';
const byteaParser = (value: string) => {
return hexToBuffer(value);
};
export const createByteaTypeParser = (): TypeParser => {
return {
name: 'bytea',
parse: byteaParser,
};
};

View File

@@ -0,0 +1,12 @@
import { TypeParser } from 'slonik';
const numericParser = (value: string) => {
return BigInt(value);
};
export const createNumericTypeParser = (): TypeParser => {
return {
name: 'numeric',
parse: numericParser,
};
};

View File

@@ -0,0 +1,3 @@
export * from './createBigIntTypeParser';
export * from './createByteaTypeParser';
export * from './createNumericTypeParser';

View File

@@ -0,0 +1,25 @@
/**
* Encodes a buffer as a `0x` prefixed lower-case hex string.
* Returns an empty string if the buffer is zero length.
*/
export function bufferToHexPrefixString(buff: Buffer): string {
if (buff.length === 0) {
return '';
}
return '\\x' + buff.toString('hex');
}
export function hexToBuffer(hex: string): Buffer {
if (hex.length === 0) {
return Buffer.alloc(0);
}
if (hex.length % 2 !== 0) {
throw new Error(`Hex string is an odd number of digits: ${hex}`);
}
if (hex.startsWith('\\x')) {
return Buffer.from(hex.substring(2), 'hex');
}
throw new Error(`Hex string is missing the "\\x" prefix: "${hex}"`);
}

View File

@@ -0,0 +1,5 @@
import { DatabasePool } from 'slonik';
export abstract class PersistentService {
abstract get pgPool(): DatabasePool;
}

View File

@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import PersistentServiceProvider from './persistent.service';
@Module({
imports: [],
exports: [PersistentServiceProvider],
providers: [PersistentServiceProvider],
})
export class PersistentModule {}

View File

@@ -0,0 +1,84 @@
import { OnModuleDestroy } from '@nestjs/common';
import { typedEnv } from '@placeholdersoft/typed-env';
import {
createDateTypeParser,
createIntervalTypeParser,
createPool,
createTimestampTypeParser,
createTimestampWithTimeZoneTypeParser,
DatabasePool,
} from 'slonik';
import { createQueryLoggingInterceptor } from 'slonik-interceptor-query-logging';
import { createResultParserInterceptor } from './interceptor/result-parser-interceptor';
import {
createByteaTypeParser,
createFloat4TypeParser,
createFloat8TypeParser,
createInt8TypeParser,
createIntegerTypeParser,
createNumericTypeParser,
} from './parsers';
import { PersistentService } from './persistent.interface';
export class DefaultPersistentService
extends PersistentService
implements OnModuleDestroy
{
private _pgPool: DatabasePool | null = null;
get pgPool(): DatabasePool {
if (this._pgPool === null) {
throw new Error('pgPool is not initialized');
}
return this._pgPool;
}
async connectPool() {
const pool = await createPool(
typedEnv('NODE_DATABASE_URL').required().toString(),
{
typeParsers: [
// - customized type parsers
createInt8TypeParser(),
createNumericTypeParser(),
createFloat4TypeParser(),
createFloat8TypeParser(),
createIntegerTypeParser(),
createByteaTypeParser(),
// - default type parsers
createDateTypeParser(),
createIntervalTypeParser(),
createTimestampTypeParser(),
createTimestampWithTimeZoneTypeParser(),
],
interceptors: [
createQueryLoggingInterceptor(),
createResultParserInterceptor(),
],
},
);
this._pgPool = pool;
}
onModuleDestroy() {
return this.pgPool.end();
}
}
const PersistentServiceProvider = {
provide: PersistentService,
useFactory: async () => {
const service = new DefaultPersistentService();
const url = typedEnv('NODE_DATABASE_URL').toString();
if (url != null && url.length > 0) {
await service.connectPool();
} else {
console.warn('NODE_DATABASE_URL is not set, skip connecting to DB');
}
return service;
},
};
export default PersistentServiceProvider;

4
tools/bin/pkg-update Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
ncu -u -x prettier