mirror of
https://github.com/placeholder-soft/sui-data-sync.git
synced 2026-04-29 04:56:05 +08:00
[WIP] - 🚧
This commit is contained in:
@@ -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
2315
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
41
src/lib/persistent/interceptor/result-parser-interceptor.ts
Normal file
41
src/lib/persistent/interceptor/result-parser-interceptor.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
};
|
||||
35
src/lib/persistent/parsers/createBigIntTypeParser.ts
Normal file
35
src/lib/persistent/parsers/createBigIntTypeParser.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
13
src/lib/persistent/parsers/createByteaTypeParser.ts
Normal file
13
src/lib/persistent/parsers/createByteaTypeParser.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
12
src/lib/persistent/parsers/createNumericTypeParser.ts
Normal file
12
src/lib/persistent/parsers/createNumericTypeParser.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
3
src/lib/persistent/parsers/index.ts
Normal file
3
src/lib/persistent/parsers/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './createBigIntTypeParser';
|
||||
export * from './createByteaTypeParser';
|
||||
export * from './createNumericTypeParser';
|
||||
25
src/lib/persistent/parsers/utils.ts
Normal file
25
src/lib/persistent/parsers/utils.ts
Normal 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}"`);
|
||||
}
|
||||
5
src/lib/persistent/persistent.interface.ts
Normal file
5
src/lib/persistent/persistent.interface.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { DatabasePool } from 'slonik';
|
||||
|
||||
export abstract class PersistentService {
|
||||
abstract get pgPool(): DatabasePool;
|
||||
}
|
||||
9
src/lib/persistent/persistent.module.ts
Normal file
9
src/lib/persistent/persistent.module.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import PersistentServiceProvider from './persistent.service';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
exports: [PersistentServiceProvider],
|
||||
providers: [PersistentServiceProvider],
|
||||
})
|
||||
export class PersistentModule {}
|
||||
84
src/lib/persistent/persistent.service.ts
Normal file
84
src/lib/persistent/persistent.service.ts
Normal 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
4
tools/bin/pkg-update
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ncu -u -x prettier
|
||||
Reference in New Issue
Block a user