@idevconn/create-icore 0.10.2 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +13 -2
- package/dist/index.cjs +12 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +12 -1
- package/package.json +1 -1
- package/templates/docker-compose.yml +14 -0
- package/templates/libs/db-strategies/postgres/eslint.config.mjs +30 -0
- package/templates/libs/db-strategies/postgres/package.json +19 -0
- package/templates/libs/db-strategies/postgres/project.json +19 -0
- package/templates/libs/db-strategies/postgres/src/index.ts +3 -0
- package/templates/libs/db-strategies/postgres/src/lib/__tests__/postgres-db.contract.unit.test.ts +4 -0
- package/templates/libs/db-strategies/postgres/src/lib/__tests__/postgres-db.module.unit.test.ts +37 -0
- package/templates/libs/db-strategies/postgres/src/lib/postgres-db.module.ts +33 -0
- package/templates/libs/db-strategies/postgres/src/lib/postgres-db.strategy.ts +139 -0
- package/templates/libs/db-strategies/postgres/src/lib/testing/mock-postgres.ts +94 -0
- package/templates/libs/db-strategies/postgres/tsconfig.json +19 -0
- package/templates/libs/db-strategies/postgres/tsconfig.lib.json +24 -0
- package/templates/libs/db-strategies/postgres/tsconfig.spec.json +22 -0
- package/templates/libs/db-strategies/postgres/vitest.config.mts +22 -0
- package/templates/tsconfig.base.json +1 -0
package/dist/cli.js
CHANGED
|
@@ -233,7 +233,8 @@ Re-run with @latest to refresh:
|
|
|
233
233
|
options: [
|
|
234
234
|
{ value: "supabase", label: "Supabase Postgres" },
|
|
235
235
|
{ value: "firebase", label: "Firestore" },
|
|
236
|
-
{ value: "mongodb", label: "MongoDB" }
|
|
236
|
+
{ value: "mongodb", label: "MongoDB" },
|
|
237
|
+
{ value: "postgres", label: "PostgreSQL (direct, postgres.js)" }
|
|
237
238
|
],
|
|
238
239
|
initialValue: authProvider
|
|
239
240
|
});
|
|
@@ -410,7 +411,7 @@ async function rewriteRootPackageJson(targetDir, opts) {
|
|
|
410
411
|
const pkg = JSON.parse(raw);
|
|
411
412
|
pkg["name"] = opts.projectName;
|
|
412
413
|
pkg["version"] = "0.0.1";
|
|
413
|
-
pkg["icoreVersion"] = true ? "0.
|
|
414
|
+
pkg["icoreVersion"] = true ? "0.11.0" : "unknown";
|
|
414
415
|
pkg["private"] = true;
|
|
415
416
|
delete pkg.description;
|
|
416
417
|
const transportDeps = TRANSPORT_DEPS[opts.transport];
|
|
@@ -1538,6 +1539,16 @@ var MANIFEST = {
|
|
|
1538
1539
|
deps: { mongoose: "^9.6.3" },
|
|
1539
1540
|
tsPaths: { "@icore/db-mongodb": ["libs/db-strategies/mongodb/src/index.ts"] },
|
|
1540
1541
|
nestModule: { importFrom: "@icore/db-mongodb", symbol: "MongoDbDbModule", into: "notes" }
|
|
1542
|
+
},
|
|
1543
|
+
postgres: {
|
|
1544
|
+
libDirs: ["libs/db-strategies/postgres"],
|
|
1545
|
+
deps: { postgres: "^3" },
|
|
1546
|
+
tsPaths: { "@icore/db-postgres": ["libs/db-strategies/postgres/src/index.ts"] },
|
|
1547
|
+
nestModule: {
|
|
1548
|
+
importFrom: "@icore/db-postgres",
|
|
1549
|
+
symbol: "PostgresDbModule",
|
|
1550
|
+
into: "notes"
|
|
1551
|
+
}
|
|
1541
1552
|
}
|
|
1542
1553
|
},
|
|
1543
1554
|
feature: {
|
package/dist/index.cjs
CHANGED
|
@@ -1249,6 +1249,16 @@ var MANIFEST = {
|
|
|
1249
1249
|
deps: { mongoose: "^9.6.3" },
|
|
1250
1250
|
tsPaths: { "@icore/db-mongodb": ["libs/db-strategies/mongodb/src/index.ts"] },
|
|
1251
1251
|
nestModule: { importFrom: "@icore/db-mongodb", symbol: "MongoDbDbModule", into: "notes" }
|
|
1252
|
+
},
|
|
1253
|
+
postgres: {
|
|
1254
|
+
libDirs: ["libs/db-strategies/postgres"],
|
|
1255
|
+
deps: { postgres: "^3" },
|
|
1256
|
+
tsPaths: { "@icore/db-postgres": ["libs/db-strategies/postgres/src/index.ts"] },
|
|
1257
|
+
nestModule: {
|
|
1258
|
+
importFrom: "@icore/db-postgres",
|
|
1259
|
+
symbol: "PostgresDbModule",
|
|
1260
|
+
into: "notes"
|
|
1261
|
+
}
|
|
1252
1262
|
}
|
|
1253
1263
|
},
|
|
1254
1264
|
feature: {
|
|
@@ -2247,7 +2257,8 @@ Re-run with @latest to refresh:
|
|
|
2247
2257
|
options: [
|
|
2248
2258
|
{ value: "supabase", label: "Supabase Postgres" },
|
|
2249
2259
|
{ value: "firebase", label: "Firestore" },
|
|
2250
|
-
{ value: "mongodb", label: "MongoDB" }
|
|
2260
|
+
{ value: "mongodb", label: "MongoDB" },
|
|
2261
|
+
{ value: "postgres", label: "PostgreSQL (direct, postgres.js)" }
|
|
2251
2262
|
],
|
|
2252
2263
|
initialValue: authProvider
|
|
2253
2264
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
type AuthBackend = 'supabase' | 'firebase' | 'mongodb';
|
|
2
2
|
type AuthProvider = AuthBackend | 'none';
|
|
3
|
-
type DbProvider = 'supabase' | 'firebase' | 'mongodb' | 'none';
|
|
3
|
+
type DbProvider = 'supabase' | 'firebase' | 'mongodb' | 'postgres' | 'none';
|
|
4
4
|
type UploadProvider = 'supabase' | 'firebase' | 'cloudinary' | 'mongodb' | 'none';
|
|
5
5
|
type PaymentProvider = 'paypal' | 'none';
|
|
6
6
|
type JobsProvider = 'bullmq' | 'none';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
type AuthBackend = 'supabase' | 'firebase' | 'mongodb';
|
|
2
2
|
type AuthProvider = AuthBackend | 'none';
|
|
3
|
-
type DbProvider = 'supabase' | 'firebase' | 'mongodb' | 'none';
|
|
3
|
+
type DbProvider = 'supabase' | 'firebase' | 'mongodb' | 'postgres' | 'none';
|
|
4
4
|
type UploadProvider = 'supabase' | 'firebase' | 'cloudinary' | 'mongodb' | 'none';
|
|
5
5
|
type PaymentProvider = 'paypal' | 'none';
|
|
6
6
|
type JobsProvider = 'bullmq' | 'none';
|
package/dist/index.js
CHANGED
|
@@ -1206,6 +1206,16 @@ var MANIFEST = {
|
|
|
1206
1206
|
deps: { mongoose: "^9.6.3" },
|
|
1207
1207
|
tsPaths: { "@icore/db-mongodb": ["libs/db-strategies/mongodb/src/index.ts"] },
|
|
1208
1208
|
nestModule: { importFrom: "@icore/db-mongodb", symbol: "MongoDbDbModule", into: "notes" }
|
|
1209
|
+
},
|
|
1210
|
+
postgres: {
|
|
1211
|
+
libDirs: ["libs/db-strategies/postgres"],
|
|
1212
|
+
deps: { postgres: "^3" },
|
|
1213
|
+
tsPaths: { "@icore/db-postgres": ["libs/db-strategies/postgres/src/index.ts"] },
|
|
1214
|
+
nestModule: {
|
|
1215
|
+
importFrom: "@icore/db-postgres",
|
|
1216
|
+
symbol: "PostgresDbModule",
|
|
1217
|
+
into: "notes"
|
|
1218
|
+
}
|
|
1209
1219
|
}
|
|
1210
1220
|
},
|
|
1211
1221
|
feature: {
|
|
@@ -2204,7 +2214,8 @@ Re-run with @latest to refresh:
|
|
|
2204
2214
|
options: [
|
|
2205
2215
|
{ value: "supabase", label: "Supabase Postgres" },
|
|
2206
2216
|
{ value: "firebase", label: "Firestore" },
|
|
2207
|
-
{ value: "mongodb", label: "MongoDB" }
|
|
2217
|
+
{ value: "mongodb", label: "MongoDB" },
|
|
2218
|
+
{ value: "postgres", label: "PostgreSQL (direct, postgres.js)" }
|
|
2208
2219
|
],
|
|
2209
2220
|
initialValue: authProvider
|
|
2210
2221
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idevconn/create-icore",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Bootstrap a new project from the iCore scaffold (Nx + NestJS + React + Vite + shadcn/Tailwind, swappable auth + storage providers).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "iDEVconn",
|
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
services:
|
|
2
|
+
postgres:
|
|
3
|
+
image: postgres:16-alpine
|
|
4
|
+
environment:
|
|
5
|
+
POSTGRES_USER: ${POSTGRES_USER:-icore}
|
|
6
|
+
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-icore}
|
|
7
|
+
POSTGRES_DB: ${POSTGRES_DB:-icore}
|
|
8
|
+
ports:
|
|
9
|
+
- '5432:5432'
|
|
10
|
+
healthcheck:
|
|
11
|
+
test: ['CMD-SHELL', 'pg_isready -U icore']
|
|
12
|
+
interval: 5s
|
|
13
|
+
retries: 10
|
|
14
|
+
networks: [icore]
|
|
15
|
+
|
|
2
16
|
redis:
|
|
3
17
|
image: redis:7-alpine
|
|
4
18
|
healthcheck:
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import baseConfig from '../../../eslint.config.mjs';
|
|
2
|
+
|
|
3
|
+
export default [
|
|
4
|
+
...baseConfig,
|
|
5
|
+
{
|
|
6
|
+
files: ['**/*.json'],
|
|
7
|
+
rules: {
|
|
8
|
+
'@nx/dependency-checks': [
|
|
9
|
+
'error',
|
|
10
|
+
{
|
|
11
|
+
ignoredFiles: [
|
|
12
|
+
'{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}',
|
|
13
|
+
'{projectRoot}/vitest.config.{js,ts,mjs,mts}',
|
|
14
|
+
],
|
|
15
|
+
ignoredDependencies: [
|
|
16
|
+
'@icore/shared',
|
|
17
|
+
'postgres',
|
|
18
|
+
'@nestjs/common',
|
|
19
|
+
'@nestjs/config',
|
|
20
|
+
'@nestjs/testing',
|
|
21
|
+
'vitest',
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
languageOptions: {
|
|
27
|
+
parser: await import('jsonc-eslint-parser'),
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@icore/db-postgres",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"@icore/shared": "*",
|
|
10
|
+
"@nestjs/common": "^11.1.27",
|
|
11
|
+
"@nestjs/config": "^4.0.4",
|
|
12
|
+
"postgres": "^3.4.5",
|
|
13
|
+
"tslib": "^2.8.1"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@nestjs/testing": "^11.1.27",
|
|
17
|
+
"vitest": "^4.1.9"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "db-postgres",
|
|
3
|
+
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
|
4
|
+
"sourceRoot": "libs/db-strategies/postgres/src",
|
|
5
|
+
"projectType": "library",
|
|
6
|
+
"tags": [],
|
|
7
|
+
"targets": {
|
|
8
|
+
"build": {
|
|
9
|
+
"executor": "@nx/js:tsc",
|
|
10
|
+
"outputs": ["{options.outputPath}"],
|
|
11
|
+
"options": {
|
|
12
|
+
"outputPath": "dist/libs/db-strategies/postgres",
|
|
13
|
+
"main": "libs/db-strategies/postgres/src/index.ts",
|
|
14
|
+
"tsConfig": "libs/db-strategies/postgres/tsconfig.lib.json",
|
|
15
|
+
"assets": ["libs/db-strategies/postgres/*.md"]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
package/templates/libs/db-strategies/postgres/src/lib/__tests__/postgres-db.module.unit.test.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { Global, Module } from '@nestjs/common';
|
|
3
|
+
import { Test } from '@nestjs/testing';
|
|
4
|
+
import { ConfigService } from '@nestjs/config';
|
|
5
|
+
import { PostgresDbModule, POSTGRES_DB_REQUIRED_ENV } from '../postgres-db.module.js';
|
|
6
|
+
import { PostgresDBStrategy } from '../postgres-db.strategy.js';
|
|
7
|
+
|
|
8
|
+
let ENV: Record<string, string | undefined> = {};
|
|
9
|
+
|
|
10
|
+
@Global()
|
|
11
|
+
@Module({
|
|
12
|
+
providers: [
|
|
13
|
+
{
|
|
14
|
+
provide: ConfigService,
|
|
15
|
+
useValue: {
|
|
16
|
+
get: (k: string) => ENV[k],
|
|
17
|
+
getOrThrow: (k: string) => ENV[k],
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
exports: [ConfigService],
|
|
22
|
+
})
|
|
23
|
+
class StubConfigModule {}
|
|
24
|
+
|
|
25
|
+
describe('PostgresDbModule', () => {
|
|
26
|
+
it('declares its required env', () => {
|
|
27
|
+
expect(POSTGRES_DB_REQUIRED_ENV).toEqual(['POSTGRES_URL']);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('provides a real PostgresDBStrategy under DBStrategy when env present', async () => {
|
|
31
|
+
ENV = { POSTGRES_URL: 'postgresql://user:pass@localhost:5432/test' };
|
|
32
|
+
const ref = await Test.createTestingModule({
|
|
33
|
+
imports: [StubConfigModule, PostgresDbModule.forRoot('.env')],
|
|
34
|
+
}).compile();
|
|
35
|
+
expect(ref.get('DBStrategy')).toBeInstanceOf(PostgresDBStrategy);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Module, DynamicModule } from '@nestjs/common';
|
|
2
|
+
import { ConfigService } from '@nestjs/config';
|
|
3
|
+
import { buildStrategyWithFallback, FakeDBStrategy } from '@icore/shared';
|
|
4
|
+
import type { DBStrategy } from '@icore/shared';
|
|
5
|
+
import { PostgresDBStrategy } from './postgres-db.strategy';
|
|
6
|
+
|
|
7
|
+
export const POSTGRES_DB_REQUIRED_ENV = ['POSTGRES_URL'];
|
|
8
|
+
|
|
9
|
+
@Module({})
|
|
10
|
+
export class PostgresDbModule {
|
|
11
|
+
static forRoot(envPath: string): DynamicModule {
|
|
12
|
+
return {
|
|
13
|
+
module: PostgresDbModule,
|
|
14
|
+
providers: [
|
|
15
|
+
{
|
|
16
|
+
provide: 'DBStrategy',
|
|
17
|
+
useFactory: (cfg: ConfigService): DBStrategy =>
|
|
18
|
+
buildStrategyWithFallback<DBStrategy>({
|
|
19
|
+
service: 'notes MS',
|
|
20
|
+
provider: 'postgres',
|
|
21
|
+
requiredEnv: POSTGRES_DB_REQUIRED_ENV,
|
|
22
|
+
cfg,
|
|
23
|
+
envPath,
|
|
24
|
+
build: () => new PostgresDBStrategy(cfg.getOrThrow<string>('POSTGRES_URL')),
|
|
25
|
+
fake: () => new FakeDBStrategy(),
|
|
26
|
+
}),
|
|
27
|
+
inject: [ConfigService],
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
exports: ['DBStrategy'],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import postgres from 'postgres';
|
|
2
|
+
import type { DBDocument, DBStrategy, QueryOptions } from '@icore/shared';
|
|
3
|
+
|
|
4
|
+
export class PostgresDBStrategy implements DBStrategy {
|
|
5
|
+
private readonly sql: postgres.Sql;
|
|
6
|
+
private readonly initializing = new Map<string, Promise<void>>();
|
|
7
|
+
private readonly initialized = new Set<string>();
|
|
8
|
+
|
|
9
|
+
constructor(url: string) {
|
|
10
|
+
this.sql = postgres(url);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
private async ensureTable(collection: string): Promise<void> {
|
|
14
|
+
if (this.initialized.has(collection)) return;
|
|
15
|
+
let inflight = this.initializing.get(collection);
|
|
16
|
+
if (inflight) return inflight;
|
|
17
|
+
inflight = this._createTable(collection).catch((err) => {
|
|
18
|
+
this.initializing.delete(collection);
|
|
19
|
+
throw err;
|
|
20
|
+
});
|
|
21
|
+
this.initializing.set(collection, inflight);
|
|
22
|
+
return inflight;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private async _createTable(collection: string): Promise<void> {
|
|
26
|
+
await this.sql`
|
|
27
|
+
CREATE TABLE IF NOT EXISTS ${this.sql(collection)} (
|
|
28
|
+
id TEXT PRIMARY KEY,
|
|
29
|
+
data JSONB NOT NULL
|
|
30
|
+
)
|
|
31
|
+
`;
|
|
32
|
+
await this.sql`
|
|
33
|
+
CREATE INDEX IF NOT EXISTS ${this.sql(collection + '_data_gin')}
|
|
34
|
+
ON ${this.sql(collection)} USING GIN (data)
|
|
35
|
+
`;
|
|
36
|
+
this.initialized.add(collection);
|
|
37
|
+
this.initializing.delete(collection);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async get<T>(collection: string, id: string): Promise<DBDocument<T> | null> {
|
|
41
|
+
await this.ensureTable(collection);
|
|
42
|
+
const rows = await this.sql<{ id: string; data: T }[]>`
|
|
43
|
+
SELECT id, data FROM ${this.sql(collection)} WHERE id = ${id}
|
|
44
|
+
`;
|
|
45
|
+
const row = rows[0];
|
|
46
|
+
if (!row) return null;
|
|
47
|
+
return { id: row.id, data: row.data };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async set<T>(collection: string, id: string, data: T): Promise<void> {
|
|
51
|
+
await this.ensureTable(collection);
|
|
52
|
+
await this.sql`
|
|
53
|
+
INSERT INTO ${this.sql(collection)} (id, data)
|
|
54
|
+
VALUES (${id}, ${this.sql.json(data as unknown as postgres.JSONValue)})
|
|
55
|
+
ON CONFLICT (id) DO UPDATE SET data = EXCLUDED.data
|
|
56
|
+
`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async update<T>(collection: string, id: string, patch: Partial<T>): Promise<void> {
|
|
60
|
+
await this.ensureTable(collection);
|
|
61
|
+
const rows = await this.sql`
|
|
62
|
+
UPDATE ${this.sql(collection)}
|
|
63
|
+
SET data = data || ${this.sql.json(patch as postgres.JSONValue)}
|
|
64
|
+
WHERE id = ${id}
|
|
65
|
+
RETURNING id
|
|
66
|
+
`;
|
|
67
|
+
if (rows.count === 0) throw new Error(`not_found: ${collection}/${id}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async delete(collection: string, id: string): Promise<void> {
|
|
71
|
+
await this.ensureTable(collection);
|
|
72
|
+
const rows = await this.sql`
|
|
73
|
+
DELETE FROM ${this.sql(collection)} WHERE id = ${id} RETURNING id
|
|
74
|
+
`;
|
|
75
|
+
if (rows.count === 0) throw new Error(`not_found: ${collection}/${id}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async list<T>(collection: string, opts?: QueryOptions): Promise<DBDocument<T>[]> {
|
|
79
|
+
await this.ensureTable(collection);
|
|
80
|
+
|
|
81
|
+
const conditions: postgres.Fragment[] = [];
|
|
82
|
+
|
|
83
|
+
if (opts?.where) {
|
|
84
|
+
for (const c of opts.where) {
|
|
85
|
+
const field = c.field;
|
|
86
|
+
if (c.op === '==' || c.op === '!=') {
|
|
87
|
+
const expr = field === 'id' ? this.sql`id` : this.sql`data->>${field}`;
|
|
88
|
+
conditions.push(
|
|
89
|
+
c.op === '=='
|
|
90
|
+
? this.sql`${expr} = ${String(c.value)}`
|
|
91
|
+
: this.sql`${expr} != ${String(c.value)}`,
|
|
92
|
+
);
|
|
93
|
+
} else if (c.op === 'in') {
|
|
94
|
+
const vals = (c.value as unknown[]).map(String);
|
|
95
|
+
const expr = field === 'id' ? this.sql`id` : this.sql`data->>${field}`;
|
|
96
|
+
conditions.push(this.sql`${expr} = ANY(${vals})`);
|
|
97
|
+
} else {
|
|
98
|
+
// numeric ops: <, <=, >, >=
|
|
99
|
+
const expr =
|
|
100
|
+
field === 'id' ? this.sql`id::numeric` : this.sql`(data->>${field})::numeric`;
|
|
101
|
+
const val = Number(c.value);
|
|
102
|
+
if (c.op === '<') conditions.push(this.sql`${expr} < ${val}`);
|
|
103
|
+
else if (c.op === '<=') conditions.push(this.sql`${expr} <= ${val}`);
|
|
104
|
+
else if (c.op === '>') conditions.push(this.sql`${expr} > ${val}`);
|
|
105
|
+
else if (c.op === '>=') conditions.push(this.sql`${expr} >= ${val}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const whereClause =
|
|
111
|
+
conditions.length > 0
|
|
112
|
+
? this.sql`WHERE ${conditions.reduce((acc, c) => this.sql`${acc} AND ${c}`)}`
|
|
113
|
+
: this.sql``;
|
|
114
|
+
|
|
115
|
+
const orderBy = opts?.orderBy;
|
|
116
|
+
const orderClause = orderBy
|
|
117
|
+
? (() => {
|
|
118
|
+
const col = orderBy.field === 'id' ? this.sql`id` : this.sql`data->>${orderBy.field}`;
|
|
119
|
+
const dir = orderBy.direction === 'desc' ? this.sql`DESC` : this.sql`ASC`;
|
|
120
|
+
return this.sql`ORDER BY ${col} ${dir}`;
|
|
121
|
+
})()
|
|
122
|
+
: this.sql``;
|
|
123
|
+
|
|
124
|
+
const limitClause = opts?.limit != null ? this.sql`LIMIT ${opts.limit}` : this.sql``;
|
|
125
|
+
|
|
126
|
+
const rows = await this.sql<{ id: string; data: T }[]>`
|
|
127
|
+
SELECT id, data FROM ${this.sql(collection)}
|
|
128
|
+
${whereClause}
|
|
129
|
+
${orderClause}
|
|
130
|
+
${limitClause}
|
|
131
|
+
`;
|
|
132
|
+
|
|
133
|
+
return rows.map((row) => ({ id: row.id, data: row.data }));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async end(): Promise<void> {
|
|
137
|
+
await this.sql.end();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { DBDocument, DBStrategy, QueryOptions } from '@icore/shared';
|
|
2
|
+
|
|
3
|
+
function applyOp(val: unknown, op: string, target: unknown): boolean {
|
|
4
|
+
switch (op) {
|
|
5
|
+
case '==':
|
|
6
|
+
return String(val) === String(target);
|
|
7
|
+
case '!=':
|
|
8
|
+
return String(val) !== String(target);
|
|
9
|
+
case '<':
|
|
10
|
+
return Number(val) < Number(target);
|
|
11
|
+
case '<=':
|
|
12
|
+
return Number(val) <= Number(target);
|
|
13
|
+
case '>':
|
|
14
|
+
return Number(val) > Number(target);
|
|
15
|
+
case '>=':
|
|
16
|
+
return Number(val) >= Number(target);
|
|
17
|
+
case 'in':
|
|
18
|
+
return Array.isArray(target) && (target as unknown[]).map(String).includes(String(val));
|
|
19
|
+
default:
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function resolveField(data: Record<string, unknown>, id: string, field: string): unknown {
|
|
25
|
+
if (field === 'id') return id;
|
|
26
|
+
return data[field];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createMockPostgresDB(): DBStrategy {
|
|
30
|
+
const store = new Map<string, Map<string, Record<string, unknown>>>();
|
|
31
|
+
|
|
32
|
+
function getTable(collection: string): Map<string, Record<string, unknown>> {
|
|
33
|
+
let t = store.get(collection);
|
|
34
|
+
if (!t) {
|
|
35
|
+
t = new Map();
|
|
36
|
+
store.set(collection, t);
|
|
37
|
+
}
|
|
38
|
+
return t;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
async get<T>(collection: string, id: string): Promise<DBDocument<T> | null> {
|
|
43
|
+
const row = getTable(collection).get(id);
|
|
44
|
+
if (!row) return null;
|
|
45
|
+
return { id, data: row as T };
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
async set<T>(collection: string, id: string, data: T): Promise<void> {
|
|
49
|
+
getTable(collection).set(id, { ...(data as Record<string, unknown>) });
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
async update<T>(collection: string, id: string, patch: Partial<T>): Promise<void> {
|
|
53
|
+
const table = getTable(collection);
|
|
54
|
+
const existing = table.get(id);
|
|
55
|
+
if (!existing) throw new Error(`not_found: ${collection}/${id}`);
|
|
56
|
+
table.set(id, { ...existing, ...(patch as Record<string, unknown>) });
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
async delete(collection: string, id: string): Promise<void> {
|
|
60
|
+
const table = getTable(collection);
|
|
61
|
+
if (!table.has(id)) throw new Error(`not_found: ${collection}/${id}`);
|
|
62
|
+
table.delete(id);
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
async list<T>(collection: string, opts?: QueryOptions): Promise<DBDocument<T>[]> {
|
|
66
|
+
let entries = [...getTable(collection).entries()].map(([id, data]) => ({ id, data }));
|
|
67
|
+
|
|
68
|
+
if (opts?.where) {
|
|
69
|
+
for (const c of opts.where) {
|
|
70
|
+
entries = entries.filter((e) => {
|
|
71
|
+
const val = resolveField(e.data, e.id, c.field);
|
|
72
|
+
return applyOp(val, c.op, c.value);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (opts?.orderBy) {
|
|
78
|
+
const { field, direction } = opts.orderBy;
|
|
79
|
+
entries.sort((a, b) => {
|
|
80
|
+
const av = resolveField(a.data, a.id, field);
|
|
81
|
+
const bv = resolveField(b.data, b.id, field);
|
|
82
|
+
const cmp = String(av) < String(bv) ? -1 : String(av) > String(bv) ? 1 : 0;
|
|
83
|
+
return direction === 'desc' ? -cmp : cmp;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (opts?.limit != null) {
|
|
88
|
+
entries = entries.slice(0, opts.limit);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return entries.map((e) => ({ id: e.id, data: e.data as T }));
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"module": "node16",
|
|
5
|
+
"moduleResolution": "node16",
|
|
6
|
+
"experimentalDecorators": true,
|
|
7
|
+
"emitDecoratorMetadata": true,
|
|
8
|
+
"forceConsistentCasingInFileNames": true,
|
|
9
|
+
"strict": true,
|
|
10
|
+
"importHelpers": true,
|
|
11
|
+
"noImplicitOverride": true,
|
|
12
|
+
"noImplicitReturns": true,
|
|
13
|
+
"noFallthroughCasesInSwitch": true,
|
|
14
|
+
"noPropertyAccessFromIndexSignature": true
|
|
15
|
+
},
|
|
16
|
+
"files": [],
|
|
17
|
+
"include": [],
|
|
18
|
+
"references": [{ "path": "./tsconfig.lib.json" }, { "path": "./tsconfig.spec.json" }]
|
|
19
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "../../../dist/out-tsc",
|
|
5
|
+
"rootDir": "../../..",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"types": ["node"]
|
|
8
|
+
},
|
|
9
|
+
"include": ["src/**/*.ts"],
|
|
10
|
+
"exclude": [
|
|
11
|
+
"vite.config.ts",
|
|
12
|
+
"vite.config.mts",
|
|
13
|
+
"vitest.config.ts",
|
|
14
|
+
"vitest.config.mts",
|
|
15
|
+
"src/**/*.test.ts",
|
|
16
|
+
"src/**/*.spec.ts",
|
|
17
|
+
"src/**/*.test.tsx",
|
|
18
|
+
"src/**/*.spec.tsx",
|
|
19
|
+
"src/**/*.test.js",
|
|
20
|
+
"src/**/*.spec.js",
|
|
21
|
+
"src/**/*.test.jsx",
|
|
22
|
+
"src/**/*.spec.jsx"
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "../../../dist/out-tsc",
|
|
5
|
+
"types": ["vitest/globals", "vitest/importMeta", "vite/client", "node", "vitest"]
|
|
6
|
+
},
|
|
7
|
+
"include": [
|
|
8
|
+
"vite.config.ts",
|
|
9
|
+
"vite.config.mts",
|
|
10
|
+
"vitest.config.ts",
|
|
11
|
+
"vitest.config.mts",
|
|
12
|
+
"src/**/*.test.ts",
|
|
13
|
+
"src/**/*.spec.ts",
|
|
14
|
+
"src/**/*.test.tsx",
|
|
15
|
+
"src/**/*.spec.tsx",
|
|
16
|
+
"src/**/*.test.js",
|
|
17
|
+
"src/**/*.spec.js",
|
|
18
|
+
"src/**/*.test.jsx",
|
|
19
|
+
"src/**/*.spec.jsx",
|
|
20
|
+
"src/**/*.d.ts"
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
|
|
3
|
+
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
|
|
4
|
+
|
|
5
|
+
export default defineConfig(() => ({
|
|
6
|
+
root: __dirname,
|
|
7
|
+
cacheDir: '../../../node_modules/.vite/libs/db-strategies/postgres',
|
|
8
|
+
plugins: [nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
|
|
9
|
+
test: {
|
|
10
|
+
name: 'db-postgres',
|
|
11
|
+
watch: false,
|
|
12
|
+
globals: true,
|
|
13
|
+
environment: 'node',
|
|
14
|
+
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
|
15
|
+
passWithNoTests: true,
|
|
16
|
+
reporters: ['default'],
|
|
17
|
+
coverage: {
|
|
18
|
+
reportsDirectory: '../../../coverage/libs/db-strategies/postgres',
|
|
19
|
+
provider: 'v8' as const,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
}));
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"@idevconn/create-icore": ["./tools/create-icore/src/index.ts"],
|
|
28
28
|
"@icore/db-supabase": ["./libs/db-strategies/supabase/src/index.ts"],
|
|
29
29
|
"@icore/db-firestore": ["./libs/db-strategies/firestore/src/index.ts"],
|
|
30
|
+
"@icore/db-postgres": ["./libs/db-strategies/postgres/src/index.ts"],
|
|
30
31
|
"@icore/payment-client": ["./libs/payment-client/src/index.ts"],
|
|
31
32
|
"@icore/notes-client": ["./libs/notes-client/src/index.ts"],
|
|
32
33
|
"@icore/jobs-client": ["./libs/jobs-client/src/index.ts"],
|