@simtlix/simfinity-postgres 3.4.2 → 3.5.1
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/README.md +2 -2
- package/package.json +3 -3
- package/src/plugin.js +2 -1
- package/src/query/compiler.js +27 -12
- package/src/schema/initialize.js +4 -2
- package/types/index.d.ts +2 -1
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ import { postgresPlugin } from '@simtlix/simfinity-postgres';
|
|
|
31
31
|
const simfinity = createSQL({ plugin: postgresPlugin({ pool, schema: 'library' }) });
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
Install `@simtlix/simfinity-sql@3.
|
|
34
|
+
Install `@simtlix/simfinity-sql@3.5.0` as a direct dependency when importing its factory. PostgreSQL 15 or later is the only supported SQL plugin. The [SQL package guide](https://github.com/simtlix/simfinity.js/tree/master/packages/sql) documents relational planning and the versioned plugin contract.
|
|
35
35
|
|
|
36
36
|
Both factories permanently bind one pool and schema. The returned runtime supports registration, generated queries and mutations, middleware and scopes, controllers and validators, custom mutations, state machines, nested relationship writes, embedded reconstruction, and native PostgreSQL model handles. Native handles expose `findById`, `find`, `create`, `update`, and `delete`; they are not Mongoose models. `find(args, { session })` accepts the generated GraphQL list query arguments. Existing `pg.Pool` configuration and `pg.PoolClient` query interfaces remain compatible across both factories.
|
|
37
37
|
|
|
@@ -60,7 +60,7 @@ The module and every `createPostgres` instance expose the shared `auth`, `valida
|
|
|
60
60
|
MCP integration is an independent opt-in and does not add MCP dependencies to PostgreSQL applications:
|
|
61
61
|
|
|
62
62
|
```sh
|
|
63
|
-
npm install @simtlix/simfinity-mcp@3.
|
|
63
|
+
npm install @simtlix/simfinity-mcp@3.5.0 @modelcontextprotocol/sdk@^1.13.0
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
```javascript
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simtlix/simfinity-postgres",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.1",
|
|
4
4
|
"description": "PostgreSQL GraphQL runtime and schema generation with foreign keys for Simfinity",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"directory": "packages/postgres"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@simtlix/simfinity-core": "3.
|
|
31
|
-
"@simtlix/simfinity-sql": "3.
|
|
30
|
+
"@simtlix/simfinity-core": "3.5.1",
|
|
31
|
+
"@simtlix/simfinity-sql": "3.5.1",
|
|
32
32
|
"pg": "^8.16.3"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
package/src/plugin.js
CHANGED
|
@@ -31,7 +31,8 @@ export const postgresPlugin = (options) => ({
|
|
|
31
31
|
begin: (client) => client.query('BEGIN ISOLATION LEVEL REPEATABLE READ'),
|
|
32
32
|
commit: (client) => client.query('COMMIT'),
|
|
33
33
|
rollback: (client) => client.query('ROLLBACK'),
|
|
34
|
-
release
|
|
34
|
+
// A release error makes pg-pool destroy a connection whose transaction may still be open.
|
|
35
|
+
release: (client, error) => client.release(error),
|
|
35
36
|
isRetryable: (error) => ['40001', '40P01'].includes(error.code),
|
|
36
37
|
normalizeError: normalizeDatabaseError,
|
|
37
38
|
},
|
package/src/query/compiler.js
CHANGED
|
@@ -153,7 +153,11 @@ export const compileQuery = (models, database, plan, extra = null) => {
|
|
|
153
153
|
}
|
|
154
154
|
unsupported('Invalid query path');
|
|
155
155
|
};
|
|
156
|
-
|
|
156
|
+
/**
|
|
157
|
+
* WHERE rejects NULL and FALSE alike, so positive scalar filters use bare, index-friendly operators.
|
|
158
|
+
* Anything under a NOT (NE, NIN or a negated group) keeps the null-safe forms that never yield NULL.
|
|
159
|
+
*/
|
|
160
|
+
const predicate = (expression, operator, value, negated = false) => {
|
|
157
161
|
const { sql, field, type, list, vector } = expression;
|
|
158
162
|
// Filters accept member names first; writes already receive internal enum values.
|
|
159
163
|
const encodeFilter = (item) => {
|
|
@@ -164,8 +168,9 @@ export const compileQuery = (models, database, plan, extra = null) => {
|
|
|
164
168
|
return encodeScalar(field, entry.value);
|
|
165
169
|
};
|
|
166
170
|
if (operator === 'LIKE' && field.scalar !== 'String') invalidValue('LIKE requires a string field');
|
|
167
|
-
const
|
|
168
|
-
const
|
|
171
|
+
const encodeValue = (item) => { const value = encodeFilter(item); return expression.dateNumeric && value ? value.getTime() : value; };
|
|
172
|
+
const scalarBind = (item) => bind(encodeValue(item), type);
|
|
173
|
+
const equality = (item, nullSafe = negated) => {
|
|
169
174
|
if (Array.isArray(item)) {
|
|
170
175
|
if (!field.list || vector) invalidValue('Array equality requires a scalar-list field');
|
|
171
176
|
return `${sql} IS NOT DISTINCT FROM ${bind(item.map(encodeFilter), `${type}[]`)}`;
|
|
@@ -174,18 +179,26 @@ export const compileQuery = (models, database, plan, extra = null) => {
|
|
|
174
179
|
const contains = `array_position(${sql}, ${scalarBind(item)}) IS NOT NULL`;
|
|
175
180
|
return item == null ? `(${sql} IS NULL OR ${contains})` : contains;
|
|
176
181
|
}
|
|
177
|
-
|
|
182
|
+
if (item == null) return `${sql} IS NULL`;
|
|
183
|
+
return `${sql} ${nullSafe ? 'IS NOT DISTINCT FROM' : '='} ${scalarBind(item)}`;
|
|
178
184
|
};
|
|
179
185
|
if (operator === 'EQ') return equality(value);
|
|
180
186
|
if (operator === 'NE') {
|
|
181
187
|
if (!list && !Array.isArray(value)) return value == null ? `${sql} IS NOT NULL` : `${sql} IS DISTINCT FROM ${scalarBind(value)}`;
|
|
182
|
-
return `NOT (${equality(value)})`;
|
|
188
|
+
return `NOT (${equality(value, true)})`;
|
|
183
189
|
}
|
|
184
190
|
if (operator === 'IN' || operator === 'NIN') {
|
|
185
|
-
|
|
186
|
-
|
|
191
|
+
if (!value.length) return operator === 'IN' ? 'FALSE' : 'TRUE';
|
|
192
|
+
let expressionSQL;
|
|
193
|
+
if (list) expressionSQL = `(${value.map((item) => equality(item)).join(' OR ')})`;
|
|
194
|
+
else {
|
|
195
|
+
// For scalar columns, one array parameter keeps large lists within the 65,535 bind limit.
|
|
196
|
+
const member = `${sql} = ANY (${bind(value.map(encodeValue), `${type}[]`)})`;
|
|
197
|
+
expressionSQL = negated || operator === 'NIN' ? `COALESCE(${member}, FALSE)` : member;
|
|
198
|
+
}
|
|
199
|
+
return operator === 'IN' ? expressionSQL : `NOT ${expressionSQL}`;
|
|
187
200
|
}
|
|
188
|
-
if (operator === 'BTW') return `(${predicate(expression, 'GTE', value[0])} AND ${predicate(expression, 'LTE', value[1])})`;
|
|
201
|
+
if (operator === 'BTW') return `(${predicate(expression, 'GTE', value[0], negated)} AND ${predicate(expression, 'LTE', value[1], negated)})`;
|
|
189
202
|
if (value == null || Array.isArray(value)) invalidValue(`${operator} requires a non-null scalar value`);
|
|
190
203
|
const parameter = scalarBind(value);
|
|
191
204
|
const compare = (left) => {
|
|
@@ -194,12 +207,14 @@ export const compileQuery = (models, database, plan, extra = null) => {
|
|
|
194
207
|
}
|
|
195
208
|
return `${left} ${{ LT: '<', LTE: '<=', GT: '>', GTE: '>=' }[operator]} ${parameter}`;
|
|
196
209
|
};
|
|
197
|
-
|
|
210
|
+
if (list) return `EXISTS (SELECT 1 FROM unnest(${sql}) v(value) WHERE ${compare('v.value')})`;
|
|
211
|
+
return negated ? `COALESCE(${compare(sql)}, FALSE)` : compare(sql);
|
|
198
212
|
};
|
|
199
|
-
|
|
213
|
+
// A negation reaches every predicate below it, including those nested in AND/OR groups.
|
|
214
|
+
const where = (node, negated = false) => {
|
|
200
215
|
if (!node) return 'TRUE';
|
|
201
|
-
if (node.kind === 'predicate') return `(${predicate(path(node.path), node.operator, node.value)})`;
|
|
202
|
-
return `(${node.terms.map(where).join(node.kind === 'and' ? ' AND ' : ' OR ')})`;
|
|
216
|
+
if (node.kind === 'predicate') return `(${predicate(path(node.path), node.operator, node.value, negated)})`;
|
|
217
|
+
return `(${node.terms.map((term) => where(term, negated)).join(node.kind === 'and' ? ' AND ' : ' OR ')})`;
|
|
203
218
|
};
|
|
204
219
|
let condition = where(plan.where);
|
|
205
220
|
if (extra) {
|
package/src/schema/initialize.js
CHANGED
|
@@ -205,6 +205,7 @@ export const initializeDatabase = async (pool, description, { mode = 'create' }
|
|
|
205
205
|
if (schema.startsWith('pg_') || schema === 'information_schema') throw new SimfinityError('System schemas cannot be managed', 'INVALID_DATABASE_SCHEMA', 400);
|
|
206
206
|
const client = await pool.connect();
|
|
207
207
|
const created = [];
|
|
208
|
+
let rollbackError;
|
|
208
209
|
try {
|
|
209
210
|
await client.query(mode === 'validate' ? 'BEGIN READ ONLY' : 'BEGIN');
|
|
210
211
|
await client.query('SET LOCAL search_path = pg_catalog');
|
|
@@ -263,9 +264,10 @@ export const initializeDatabase = async (pool, description, { mode = 'create' }
|
|
|
263
264
|
await client.query('COMMIT');
|
|
264
265
|
return { mode, created };
|
|
265
266
|
} catch (error) {
|
|
266
|
-
await client.query('ROLLBACK')
|
|
267
|
+
try { await client.query('ROLLBACK'); } catch (failure) { rollbackError = failure; }
|
|
267
268
|
throw error;
|
|
268
269
|
} finally {
|
|
269
|
-
|
|
270
|
+
// A failed ROLLBACK can leave the transaction open; the error makes pg-pool destroy the connection.
|
|
271
|
+
client.release(rollbackError);
|
|
270
272
|
}
|
|
271
273
|
};
|
package/types/index.d.ts
CHANGED
|
@@ -105,7 +105,8 @@ export interface DatabaseDescription {
|
|
|
105
105
|
export interface DatabasePool {
|
|
106
106
|
connect(): Promise<{
|
|
107
107
|
query(sql: string, values?: unknown[]): Promise<{ rows: any[]; rowCount: number | null }>;
|
|
108
|
-
|
|
108
|
+
/** An error means the connection must be destroyed instead of reused. */
|
|
109
|
+
release(error?: unknown): void;
|
|
109
110
|
}>;
|
|
110
111
|
}
|
|
111
112
|
export function describeDatabase(registrations: ModelRegistration[], options?: { schema?: string }): DatabaseDescription;
|