supalite 0.5.4 β 0.5.6
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/CHANGELOG.md +10 -0
- package/README.md +13 -1
- package/dist/postgres-client.d.ts +24 -7
- package/dist/postgres-client.js +145 -39
- package/dist/query-builder.js +12 -1
- package/docs/changelog/2025-11-26-rpc-single-support.md +29 -0
- package/docs/changelog/2025-12-17-embed-many-to-one.md +30 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.6] - 2025-12-17
|
|
4
|
+
|
|
5
|
+
### π Fixed
|
|
6
|
+
- `select()`μ PostgREST-style embed(`related_table(*)`)κ° **μλ°©ν₯ FK**λ₯Ό μ§μνλλ‘ κ°μ νμ΅λλ€. μ΄μ 1:N κ΄κ³λ λ°°μ΄(`[]` κΈ°λ³Έκ°), N:1 κ΄κ³λ κ°μ²΄(λλ `null`)λ‘ λ°νν©λλ€. (See [docs/changelog/2025-12-17-embed-many-to-one.md](docs/changelog/2025-12-17-embed-many-to-one.md))
|
|
7
|
+
|
|
8
|
+
## [0.5.5] - 2025-11-26
|
|
9
|
+
|
|
10
|
+
### β¨ Added
|
|
11
|
+
- `rpc()` λ©μλ νΈμΆ μ `.single()` λ° `.maybeSingle()` λ©μλ 체μ΄λ μ§μμ μΆκ°νμ΅λλ€. μ΄λ₯Ό ν΅ν΄ RPC κ²°κ³Όμ λν΄ λ¨μΌ ν μ μ½ μ‘°κ±΄μ μ μ©ν μ μμ΅λλ€. (See [docs/changelog/2025-11-26-rpc-single-support.md](docs/changelog/2025-11-26-rpc-single-support.md))
|
|
12
|
+
|
|
3
13
|
## [0.5.2] - 2025-10-16
|
|
4
14
|
|
|
5
15
|
### π Fixed
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# SupaLite
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/supalite)
|
|
4
4
|
|
|
5
5
|
κ°λ³κ³ ν¨μ¨μ μΈ PostgreSQL ν΄λΌμ΄μΈνΈ λΌμ΄λΈλ¬λ¦¬μ
λλ€. Supabaseμ λμΌν APIλ₯Ό μ 곡νλ©΄μλ λ κ°λ³κ³ λΉ λ₯Έ ꡬνμ μ 곡ν©λλ€.
|
|
6
6
|
|
|
@@ -213,6 +213,17 @@ const { data, error } = await client
|
|
|
213
213
|
.from('users')
|
|
214
214
|
.ilike('email', '%@example.com');
|
|
215
215
|
|
|
216
|
+
// κ΄κ³ ν
μ΄λΈ μ‘°ν (PostgREST-style embed)
|
|
217
|
+
// 1:N κ΄κ³λ λ°°μ΄λ‘ λ°νλ©λλ€ (κΈ°λ³Έκ°: [])
|
|
218
|
+
const { data: authors } = await client
|
|
219
|
+
.from('authors')
|
|
220
|
+
.select('*, books(*)');
|
|
221
|
+
|
|
222
|
+
// N:1 κ΄κ³λ κ°μ²΄λ‘ λ°νλ©λλ€ (λλ null)
|
|
223
|
+
const { data: books } = await client
|
|
224
|
+
.from('books')
|
|
225
|
+
.select('*, authors(*)');
|
|
226
|
+
|
|
216
227
|
// μ νν μΉ΄μ΄νΈμ ν¨κ» μ‘°ν
|
|
217
228
|
const { data, count, error } = await client
|
|
218
229
|
.from('users')
|
|
@@ -270,6 +281,7 @@ const { data, error } = await client
|
|
|
270
281
|
- `select(columns?: string, options?: { count?: 'exact' | 'planned' | 'estimated', head?: boolean })`: μ‘°νν μ»¬λΌ μ§μ
|
|
271
282
|
- `options.count`: `'exact'`λ‘ μ€μ νλ©΄ `limit`μ μν₯μ λ°μ§ μλ μ 체 κ²°κ³Όμ κ°μλ₯Ό `count` μμ±μΌλ‘ λ°νν©λλ€.
|
|
272
283
|
- `options.head`: `true`λ‘ μ€μ νλ©΄ λ°μ΄ν° μμ΄ `count`λ§ κ°μ Έμ΅λλ€. `count` μ΅μ
κ³Ό ν¨κ» μ¬μ©νλ©΄ ν¨μ¨μ μΌλ‘ μ 체 κ°μλ§ μ‘°νν μ μμ΅λλ€.
|
|
284
|
+
- PostgREST-style embed: `select('*, related_table(*)')` λλ `select('col, related_table(col1, col2)')`
|
|
273
285
|
- `insert(data: T['Tables'][K]['Insert'] | T['Tables'][K]['Insert'][])`: λ¨μΌ λλ λ€μ€ λ μ½λ μ½μ
|
|
274
286
|
- `update(data: T['Tables'][K]['Update'])`: λ μ½λ μ
λ°μ΄νΈ
|
|
275
287
|
- `delete()`: λ μ½λ μμ
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Pool } from 'pg';
|
|
1
2
|
import { QueryBuilder } from './query-builder';
|
|
2
3
|
import { PostgresError } from './errors';
|
|
3
4
|
import { TableOrViewName, SupaliteConfig, Row, QueryResult, SingleQueryResult } from './types';
|
|
@@ -19,6 +20,27 @@ type SchemaWithTables = {
|
|
|
19
20
|
Enums?: any;
|
|
20
21
|
CompositeTypes?: any;
|
|
21
22
|
};
|
|
23
|
+
export declare class RpcBuilder implements Promise<any> {
|
|
24
|
+
private pool;
|
|
25
|
+
private schema;
|
|
26
|
+
private procedureName;
|
|
27
|
+
private params;
|
|
28
|
+
readonly [Symbol.toStringTag] = "RpcBuilder";
|
|
29
|
+
private singleMode;
|
|
30
|
+
constructor(pool: Pool, schema: string, procedureName: string, params?: Record<string, any>);
|
|
31
|
+
single(): this;
|
|
32
|
+
maybeSingle(): this;
|
|
33
|
+
then<TResult1 = any, TResult2 = never>(onfulfilled?: ((value: any) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null): Promise<TResult1 | TResult2>;
|
|
34
|
+
catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null): Promise<any | TResult>;
|
|
35
|
+
finally(onfinally?: (() => void) | null): Promise<any>;
|
|
36
|
+
execute(): Promise<{
|
|
37
|
+
data: any;
|
|
38
|
+
error: PostgresError | null;
|
|
39
|
+
count?: number | null;
|
|
40
|
+
status?: number;
|
|
41
|
+
statusText?: string;
|
|
42
|
+
}>;
|
|
43
|
+
}
|
|
22
44
|
export declare class SupaLitePG<T extends {
|
|
23
45
|
[K: string]: SchemaWithTables;
|
|
24
46
|
}> {
|
|
@@ -45,14 +67,9 @@ export declare class SupaLitePG<T extends {
|
|
|
45
67
|
getForeignKey(schema: string, table: string, foreignTable: string): Promise<{
|
|
46
68
|
column: string;
|
|
47
69
|
foreignColumn: string;
|
|
70
|
+
isArray: boolean;
|
|
48
71
|
} | null>;
|
|
49
|
-
rpc(procedureName: string, params?: Record<string, any>):
|
|
50
|
-
data: any;
|
|
51
|
-
error: PostgresError | null;
|
|
52
|
-
count?: number | null;
|
|
53
|
-
status?: number;
|
|
54
|
-
statusText?: string;
|
|
55
|
-
}>;
|
|
72
|
+
rpc(procedureName: string, params?: Record<string, any>): RpcBuilder;
|
|
56
73
|
testConnection(): Promise<boolean>;
|
|
57
74
|
close(): Promise<void>;
|
|
58
75
|
}
|
package/dist/postgres-client.js
CHANGED
|
@@ -1,12 +1,139 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var _a;
|
|
2
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.supalitePg = exports.SupaLitePG = void 0;
|
|
4
|
+
exports.supalitePg = exports.SupaLitePG = exports.RpcBuilder = void 0;
|
|
4
5
|
const pg_1 = require("pg"); // PoolConfig μΆκ°
|
|
5
6
|
const query_builder_1 = require("./query-builder");
|
|
6
7
|
const errors_1 = require("./errors");
|
|
7
8
|
const dotenv_1 = require("dotenv");
|
|
8
9
|
// .env νμΌ λ‘λ
|
|
9
10
|
(0, dotenv_1.config)();
|
|
11
|
+
class RpcBuilder {
|
|
12
|
+
constructor(pool, schema, procedureName, params = {}) {
|
|
13
|
+
this.pool = pool;
|
|
14
|
+
this.schema = schema;
|
|
15
|
+
this.procedureName = procedureName;
|
|
16
|
+
this.params = params;
|
|
17
|
+
this[_a] = 'RpcBuilder';
|
|
18
|
+
this.singleMode = null;
|
|
19
|
+
}
|
|
20
|
+
single() {
|
|
21
|
+
this.singleMode = 'strict';
|
|
22
|
+
return this;
|
|
23
|
+
}
|
|
24
|
+
maybeSingle() {
|
|
25
|
+
this.singleMode = 'maybe';
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
then(onfulfilled, onrejected) {
|
|
29
|
+
return this.execute().then(onfulfilled, onrejected);
|
|
30
|
+
}
|
|
31
|
+
catch(onrejected) {
|
|
32
|
+
return this.execute().catch(onrejected);
|
|
33
|
+
}
|
|
34
|
+
finally(onfinally) {
|
|
35
|
+
return this.execute().finally(onfinally);
|
|
36
|
+
}
|
|
37
|
+
async execute() {
|
|
38
|
+
try {
|
|
39
|
+
const paramNames = Object.keys(this.params);
|
|
40
|
+
const paramValues = Object.values(this.params);
|
|
41
|
+
const paramPlaceholders = paramNames.length > 0
|
|
42
|
+
? paramNames.map((name, i) => `"${name}" := $${i + 1}`).join(', ')
|
|
43
|
+
: '';
|
|
44
|
+
const query = paramPlaceholders
|
|
45
|
+
? `SELECT * FROM "${this.schema}"."${this.procedureName}"(${paramPlaceholders})`
|
|
46
|
+
: `SELECT * FROM "${this.schema}"."${this.procedureName}"()`;
|
|
47
|
+
const result = await this.pool.query(query, paramValues);
|
|
48
|
+
// Handle scalar return values (Supabase special handling)
|
|
49
|
+
// If result has 1 row and 1 column, and we are not in strict table mode (which rpc generally isn't),
|
|
50
|
+
// we check if it looks like a scalar return.
|
|
51
|
+
// However, if single() is called, we must respect row constraints.
|
|
52
|
+
let data = result.rows;
|
|
53
|
+
// Unwrapping logic for scalar functions (legacy Supabase behavior emulation)
|
|
54
|
+
// If it returns a single row with a single column, treat as scalar IF not forcing array via logic.
|
|
55
|
+
// But here we'll stick to basic row handling first, then apply singleMode.
|
|
56
|
+
// NOTE: Original logic had:
|
|
57
|
+
// if (result.rows.length === 1 && Object.keys(result.rows[0]).length === 1) { ... return single value ... }
|
|
58
|
+
// This implies unwrapping happens by default if it looks like a scalar.
|
|
59
|
+
const isScalarCandidate = result.rows.length === 1 && Object.keys(result.rows[0]).length === 1;
|
|
60
|
+
if (this.singleMode) {
|
|
61
|
+
if (result.rows.length > 1) {
|
|
62
|
+
return {
|
|
63
|
+
data: null,
|
|
64
|
+
error: new errors_1.PostgresError('PGRST114: Multiple rows returned'),
|
|
65
|
+
count: null,
|
|
66
|
+
status: 406,
|
|
67
|
+
statusText: 'Not Acceptable. Expected a single row but found multiple.'
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (result.rows.length === 0) {
|
|
71
|
+
if (this.singleMode === 'strict') {
|
|
72
|
+
return {
|
|
73
|
+
data: null,
|
|
74
|
+
error: new errors_1.PostgresError('PGRST116: No rows found'),
|
|
75
|
+
count: null,
|
|
76
|
+
status: 404,
|
|
77
|
+
statusText: 'Not Found. Expected a single row but found no rows.'
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
// maybeSingle -> null data, no error
|
|
81
|
+
return {
|
|
82
|
+
data: null,
|
|
83
|
+
error: null,
|
|
84
|
+
count: 0,
|
|
85
|
+
status: 200,
|
|
86
|
+
statusText: 'OK'
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
// 1 row found
|
|
90
|
+
// Check for scalar unwrapping
|
|
91
|
+
if (Object.keys(result.rows[0]).length === 1) {
|
|
92
|
+
data = Object.values(result.rows[0])[0];
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
data = result.rows[0];
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
data,
|
|
99
|
+
error: null,
|
|
100
|
+
count: 1,
|
|
101
|
+
status: 200,
|
|
102
|
+
statusText: 'OK'
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// Default behavior (no .single() called)
|
|
106
|
+
if (isScalarCandidate) {
|
|
107
|
+
data = Object.values(result.rows[0])[0];
|
|
108
|
+
return {
|
|
109
|
+
data,
|
|
110
|
+
error: null,
|
|
111
|
+
count: 1,
|
|
112
|
+
status: 200,
|
|
113
|
+
statusText: 'OK'
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
data: result.rows.length > 0 ? result.rows : null,
|
|
118
|
+
error: null,
|
|
119
|
+
count: result.rowCount,
|
|
120
|
+
status: 200,
|
|
121
|
+
statusText: 'OK'
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
return {
|
|
126
|
+
data: null,
|
|
127
|
+
error: new errors_1.PostgresError(err.message, err.code),
|
|
128
|
+
count: null,
|
|
129
|
+
status: 500,
|
|
130
|
+
statusText: 'Internal Server Error'
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
exports.RpcBuilder = RpcBuilder;
|
|
136
|
+
_a = Symbol.toStringTag;
|
|
10
137
|
class SupaLitePG {
|
|
11
138
|
constructor(config) {
|
|
12
139
|
this.client = null;
|
|
@@ -202,11 +329,26 @@ class SupaLitePG {
|
|
|
202
329
|
`;
|
|
203
330
|
const activeClient = this.isTransaction && this.client ? this.client : await this.pool.connect();
|
|
204
331
|
try {
|
|
332
|
+
// 1) One-to-many: `foreignTable` has a foreign key referencing `table`
|
|
333
|
+
// e.g. authors <- books.author_id, so embedding books(*) on authors returns an array
|
|
205
334
|
const result = await activeClient.query(query, [schema, foreignTable, table]);
|
|
206
335
|
if (result.rows.length > 0) {
|
|
207
336
|
const relationship = {
|
|
208
337
|
column: result.rows[0].foreign_column_name,
|
|
209
338
|
foreignColumn: result.rows[0].column_name,
|
|
339
|
+
isArray: true,
|
|
340
|
+
};
|
|
341
|
+
this.foreignKeyCache.set(cacheKey, relationship);
|
|
342
|
+
return relationship;
|
|
343
|
+
}
|
|
344
|
+
// 2) Many-to-one: `table` has a foreign key referencing `foreignTable`
|
|
345
|
+
// e.g. books.author_id -> authors.id, so embedding authors(*) on books returns an object
|
|
346
|
+
const reverseResult = await activeClient.query(query, [schema, table, foreignTable]);
|
|
347
|
+
if (reverseResult.rows.length > 0) {
|
|
348
|
+
const relationship = {
|
|
349
|
+
column: reverseResult.rows[0].column_name,
|
|
350
|
+
foreignColumn: reverseResult.rows[0].foreign_column_name,
|
|
351
|
+
isArray: false,
|
|
210
352
|
};
|
|
211
353
|
this.foreignKeyCache.set(cacheKey, relationship);
|
|
212
354
|
return relationship;
|
|
@@ -220,44 +362,8 @@ class SupaLitePG {
|
|
|
220
362
|
this.foreignKeyCache.set(cacheKey, null);
|
|
221
363
|
return null;
|
|
222
364
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const paramNames = Object.keys(params);
|
|
226
|
-
const paramValues = Object.values(params);
|
|
227
|
-
const paramPlaceholders = paramNames.length > 0
|
|
228
|
-
? paramNames.map((name, i) => `"${name}" := $${i + 1}`).join(', ')
|
|
229
|
-
: '';
|
|
230
|
-
const query = paramPlaceholders
|
|
231
|
-
? `SELECT * FROM "${this.schema}"."${procedureName}"(${paramPlaceholders})`
|
|
232
|
-
: `SELECT * FROM "${this.schema}"."${procedureName}"()`;
|
|
233
|
-
const result = await this.pool.query(query, paramValues);
|
|
234
|
-
if (result.rows.length === 1 && Object.keys(result.rows[0]).length === 1) {
|
|
235
|
-
const singleValue = Object.values(result.rows[0])[0];
|
|
236
|
-
return {
|
|
237
|
-
data: singleValue,
|
|
238
|
-
error: null,
|
|
239
|
-
count: 1,
|
|
240
|
-
status: 200,
|
|
241
|
-
statusText: 'OK'
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
return {
|
|
245
|
-
data: result.rows.length > 0 ? result.rows : null,
|
|
246
|
-
error: null,
|
|
247
|
-
count: result.rowCount,
|
|
248
|
-
status: 200,
|
|
249
|
-
statusText: 'OK'
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
catch (err) {
|
|
253
|
-
return {
|
|
254
|
-
data: null,
|
|
255
|
-
error: new errors_1.PostgresError(err.message, err.code),
|
|
256
|
-
count: null,
|
|
257
|
-
status: 500,
|
|
258
|
-
statusText: 'Internal Server Error'
|
|
259
|
-
};
|
|
260
|
-
}
|
|
365
|
+
rpc(procedureName, params = {}) {
|
|
366
|
+
return new RpcBuilder(this.pool, this.schema, procedureName, params);
|
|
261
367
|
}
|
|
262
368
|
// μ°κ²° ν
μ€νΈ λ©μλ
|
|
263
369
|
async testConnection() {
|
package/dist/query-builder.js
CHANGED
|
@@ -280,12 +280,23 @@ class QueryBuilder {
|
|
|
280
280
|
return null;
|
|
281
281
|
}
|
|
282
282
|
const foreignSchemaTable = `"${String(this.schema)}"."${join.foreignTable}"`;
|
|
283
|
+
if (fk.isArray) {
|
|
284
|
+
return `(
|
|
285
|
+
SELECT COALESCE(json_agg(j), '[]'::json)
|
|
286
|
+
FROM (
|
|
287
|
+
SELECT ${join.columns}
|
|
288
|
+
FROM ${foreignSchemaTable}
|
|
289
|
+
WHERE "${fk.foreignColumn}" = ${schemaTable}."${fk.column}"
|
|
290
|
+
) as j
|
|
291
|
+
) as "${join.foreignTable}"`;
|
|
292
|
+
}
|
|
283
293
|
return `(
|
|
284
|
-
SELECT
|
|
294
|
+
SELECT row_to_json(j)
|
|
285
295
|
FROM (
|
|
286
296
|
SELECT ${join.columns}
|
|
287
297
|
FROM ${foreignSchemaTable}
|
|
288
298
|
WHERE "${fk.foreignColumn}" = ${schemaTable}."${fk.column}"
|
|
299
|
+
LIMIT 1
|
|
289
300
|
) as j
|
|
290
301
|
) as "${join.foreignTable}"`;
|
|
291
302
|
}));
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# RPC .single() and .maybeSingle() Support
|
|
2
|
+
|
|
3
|
+
- **Date**: 2025-11-26
|
|
4
|
+
- **Author**: Cline
|
|
5
|
+
- **Status**: Completed
|
|
6
|
+
|
|
7
|
+
## Summary
|
|
8
|
+
|
|
9
|
+
Implemented `.single()` and `.maybeSingle()` method chaining support for `rpc` calls in `SupaLitePG`. This brings the `rpc` method closer to the Supabase JS client API, allowing users to enforce single-row constraints on RPC results.
|
|
10
|
+
|
|
11
|
+
## Changes
|
|
12
|
+
|
|
13
|
+
1. **Refactored `rpc` Method**:
|
|
14
|
+
- The `rpc` method in `src/postgres-client.ts` now returns an instance of `RpcBuilder` instead of a `Promise` directly.
|
|
15
|
+
- `RpcBuilder` implements the `Promise` interface, ensuring backward compatibility for `await rpc(...)` usage.
|
|
16
|
+
|
|
17
|
+
2. **Introduced `RpcBuilder` Class**:
|
|
18
|
+
- Encapsulates RPC parameters and execution logic.
|
|
19
|
+
- Adds `single()` method: Expects exactly one row. Throws `PGRST116` if 0 rows, `PGRST114` if >1 rows.
|
|
20
|
+
- Adds `maybeSingle()` method: Expects at most one row. Returns `null` if 0 rows, throws `PGRST114` if >1 rows.
|
|
21
|
+
- Preserves existing scalar unwrapping logic for single-row, single-column results.
|
|
22
|
+
|
|
23
|
+
3. **Unit Tests**:
|
|
24
|
+
- Added `src/__tests__/rpc.test.ts` covering various scenarios for standard calls, `.single()`, and `.maybeSingle()`.
|
|
25
|
+
|
|
26
|
+
## Impact
|
|
27
|
+
|
|
28
|
+
- Users can now use `.single()` on `rpc` calls, resolving the `TypeError: ...single is not a function` error.
|
|
29
|
+
- Existing code using `await rpc(...)` continues to work without changes.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# PostgREST-style Embed: Many-to-One Support
|
|
2
|
+
|
|
3
|
+
- **Date**: 2025-12-17
|
|
4
|
+
- **Author**: Codex
|
|
5
|
+
- **Status**: Completed
|
|
6
|
+
|
|
7
|
+
## Summary
|
|
8
|
+
|
|
9
|
+
Fixed PostgREST-style embed syntax in `select()` (e.g. `related_table(*)`) so it works for both relationship directions:
|
|
10
|
+
|
|
11
|
+
- **1:N** (foreign table references the base table) returns an **array** (defaults to `[]`).
|
|
12
|
+
- **N:1** (base table references the foreign table) returns a **single object** (or `null`).
|
|
13
|
+
|
|
14
|
+
## Changes
|
|
15
|
+
|
|
16
|
+
1. **Bidirectional FK resolution**
|
|
17
|
+
- `SupaLitePG.getForeignKey()` now checks both directions between `table` and `foreignTable` and returns whether the embed should be an array or object.
|
|
18
|
+
|
|
19
|
+
2. **Correct JSON shape in SQL generation**
|
|
20
|
+
- `QueryBuilder` uses `json_agg` (with `COALESCE(..., '[]'::json)`) for 1:N embeds.
|
|
21
|
+
- `QueryBuilder` uses `row_to_json` (with `LIMIT 1`) for N:1 embeds.
|
|
22
|
+
|
|
23
|
+
3. **Unit tests**
|
|
24
|
+
- Added tests to cover N:1 embed behavior and nested column selection.
|
|
25
|
+
|
|
26
|
+
## Impact
|
|
27
|
+
|
|
28
|
+
- Queries like `from('menu_item_opts').select('*, menu_item_opts_schema(*)')` now embed `menu_item_opts_schema` without warnings, matching PostgREST expectations.
|
|
29
|
+
- Existing 1:N embed behavior remains compatible, with an improved empty-result shape (`[]` instead of `null`).
|
|
30
|
+
|