couchset 0.5.0 → 0.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 CHANGED
@@ -20,6 +20,7 @@ The additive client-owned primitives—typed definitions, explicit provisioning,
20
20
  - [Connection Lifecycle](#connection-lifecycle)
21
21
  - [Models](#models)
22
22
  - [Reads](#reads)
23
+ - [Typed Joins and Read Consistency](#typed-joins-and-read-consistency)
23
24
  - [Document Modeling](./docs/document-modeling.md)
24
25
  - [Writes](#writes)
25
26
  - [Queries](#queries)
@@ -233,7 +234,15 @@ await startCouchbase();
233
234
  await startCouchbaseServerless();
234
235
  ```
235
236
 
236
- The starters read `COUCHBASE_URL`, `COUCHBASE_BUCKET`, `COUCHBASE_USERNAME`, `COUCHBASE_PASSWORD`, and `COUCHBASE_PROXY`. You can pass any `CouchsetArgs` field as an override.
237
+ The starters accept one PostgreSQL-style `DB_URL` in place of the four connection settings. It takes precedence over `COUCHBASE_URL`, `COUCHBASE_BUCKET`, `COUCHBASE_USERNAME`, and `COUCHBASE_PASSWORD`; explicit `startCouchbase({...})` options still take precedence over either environment form.
238
+
239
+ ```bash
240
+ DB_URL=couchbase://user:password@localhost/bucket
241
+ DB_URL=couchbase://user:password@localhost:11210/bucket
242
+ DB_URL=couchbases://user:password@cb.example.com/bucket
243
+ ```
244
+
245
+ Use `couchbases://` for TLS. Percent-encode reserved characters in usernames, passwords, and bucket names (for example, `p%40ss` for `p@ss`). `COUCHBASE_PROXY` remains available as a separate optional setting.
237
246
 
238
247
  ## Models
239
248
 
@@ -313,6 +322,89 @@ await users.onlyDeleted().findMany<User>();
313
322
  await users.withoutDefaultWhere().findMany<User>();
314
323
  ```
315
324
 
325
+ ## Typed Joins and Read Consistency
326
+
327
+ Client-bound models infer root and related fields. Using the client-owned `db`
328
+ above, join profiles to battles by arbitrary fields:
329
+
330
+ ```ts
331
+ import {dateCodec, defineModel, joinField} from 'couchset/next';
332
+ import {QueryScanConsistency} from 'couchbase';
333
+
334
+ const battles = db.model(defineModel<{
335
+ createdByUserId: string;
336
+ startsAt: Date;
337
+ }>({name: 'Battle', scope: 'app', collection: 'battles', codecs: {startsAt: dateCodec}}));
338
+
339
+ const profiles = db.model(defineModel<{
340
+ ownerUserId: string;
341
+ displayName: string;
342
+ birthday: Date;
343
+ }>({name: 'Profile', scope: 'app', collection: 'profiles', codecs: {birthday: dateCodec}}));
344
+
345
+ const joined = await battles.page({
346
+ sourceAlias: 'battle',
347
+ include: [{
348
+ as: 'creator',
349
+ model: profiles,
350
+ type: 'leftJoin',
351
+ on: {
352
+ left: joinField('creator.ownerUserId'),
353
+ op: '$eq',
354
+ right: joinField('battle.createdByUserId'),
355
+ },
356
+ }],
357
+ limit: 20,
358
+ queryOptions: {scanConsistency: QueryScanConsistency.RequestPlus},
359
+ });
360
+
361
+ joined.items[0]?.startsAt; // Date | undefined
362
+ joined.items[0]?.creator?.birthday; // Date | undefined; related codec applied
363
+ ```
364
+
365
+ Provision the collections and appropriate indexes explicitly before querying.
366
+ `joinField()` denotes a field reference; an ordinary string is a bound value.
367
+ Use `$and` / `$or` to combine predicates. Existing `key` / `keys` includes still
368
+ use `ON KEYS`.
369
+
370
+ Use NEST for arrays, and select just the fields needed:
371
+
372
+ ```ts
373
+ const summaries = await battles.findMany({
374
+ select: ['startsAt'],
375
+ include: [{
376
+ as: 'creators',
377
+ model: profiles,
378
+ type: 'leftNest',
379
+ on: {left: joinField('creators.ownerUserId'), op: '$eq', right: joinField('doc.createdByUserId')},
380
+ select: ['displayName', 'birthday'],
381
+ }],
382
+ });
383
+ // {startsAt: Date; creators: {displayName: string; birthday: Date}[]}[]
384
+ ```
385
+
386
+ LEFT JOIN may omit its related property; LEFT NEST returns `[]` when unmatched.
387
+ JOIN preserves one row per match, including repeated root IDs. Choose an alias
388
+ that does not overwrite a root field. NEST does not promise source-key ordering.
389
+
390
+ For read-your-writes, pass tokens from successful SDK mutations:
391
+
392
+ ```ts
393
+ import {MutationState, MutationToken} from 'couchbase';
394
+
395
+ async function readAfterWrites(tokens: MutationToken[]) {
396
+ return battles.findMany({
397
+ queryOptions: {consistentWith: new MutationState(...tokens)},
398
+ });
399
+ }
400
+ // Call with SDK mutationResult.token values, not whole mutation results.
401
+ ```
402
+
403
+ `request_plus` and `consistentWith` are alternatives; neither is a multi-document
404
+ transaction. Defaults are unchanged when no consistency option is supplied.
405
+ See [joined reads and consistency](./docs/joins-and-consistency.md) for predicates,
406
+ projection limits, codecs, compatibility changes, and live validation results.
407
+
316
408
  ## Writes
317
409
 
318
410
  ```ts
@@ -3,6 +3,12 @@ export type CouchbaseStarterLogger = ((...args: any[]) => void) | false;
3
3
  export interface StartCouchbaseOptions extends Partial<CouchsetArgs> {
4
4
  logger?: CouchbaseStarterLogger;
5
5
  }
6
+ /**
7
+ * Convert a PostgreSQL-style DB_URL into Couchbase SDK connection settings.
8
+ * The bucket lives in the URL path; it is not part of Couchbase's connection
9
+ * string, which contains only the scheme, host, optional port, and options.
10
+ */
11
+ export declare const parseDatabaseUrl: (value: string) => Pick<CouchsetArgs, "bucketName" | "connectionString" | "password" | "username">;
6
12
  export declare const getConnectionOptions: (overrides?: Partial<CouchsetArgs>) => CouchsetArgs;
7
13
  export declare const connectionOptions: CouchsetArgs;
8
14
  export declare const startCouchbase: (options?: StartCouchbaseOptions) => Promise<boolean>;
package/dist/database.js CHANGED
@@ -86,13 +86,61 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
86
86
  return (mod && mod.__esModule) ? mod : { "default": mod };
87
87
  };
88
88
  Object.defineProperty(exports, "__esModule", { value: true });
89
- exports.startCouchbaseServerless = exports.startCouchbase = exports.connectionOptions = exports.getConnectionOptions = void 0;
89
+ exports.startCouchbaseServerless = exports.startCouchbase = exports.connectionOptions = exports.getConnectionOptions = exports.parseDatabaseUrl = void 0;
90
90
  var connection_1 = __importDefault(require("./connection"));
91
91
  var envValue = function (key, fallback) {
92
92
  if (fallback === void 0) { fallback = ''; }
93
93
  var value = typeof process !== 'undefined' && process.env ? process.env[key] : undefined;
94
94
  return value === undefined ? fallback : value;
95
95
  };
96
+ var databaseUrlError = function (reason) {
97
+ return new Error("DB_URL must use couchbase:// or couchbases://user:password@host/bucket: ".concat(reason));
98
+ };
99
+ var decodeDatabaseUrlComponent = function (value, label) {
100
+ try {
101
+ return decodeURIComponent(value);
102
+ }
103
+ catch (_error) {
104
+ throw databaseUrlError("".concat(label, " contains invalid percent-encoding"));
105
+ }
106
+ };
107
+ /**
108
+ * Convert a PostgreSQL-style DB_URL into Couchbase SDK connection settings.
109
+ * The bucket lives in the URL path; it is not part of Couchbase's connection
110
+ * string, which contains only the scheme, host, optional port, and options.
111
+ */
112
+ var parseDatabaseUrl = function (value) {
113
+ var url;
114
+ try {
115
+ url = new URL(value);
116
+ }
117
+ catch (_error) {
118
+ throw databaseUrlError('the value is not a valid URL');
119
+ }
120
+ if (url.protocol !== 'couchbase:' && url.protocol !== 'couchbases:') {
121
+ throw databaseUrlError("unsupported protocol ".concat(url.protocol || '(missing)'));
122
+ }
123
+ if (!url.hostname) {
124
+ throw databaseUrlError('a host is required');
125
+ }
126
+ if (!url.username || !url.password) {
127
+ throw databaseUrlError('a username and password are required');
128
+ }
129
+ if (url.hash) {
130
+ throw databaseUrlError('fragments are not supported');
131
+ }
132
+ var bucketName = decodeDatabaseUrlComponent(url.pathname.replace(/^\//, ''), 'the bucket name');
133
+ if (!bucketName || bucketName.indexOf('/') !== -1) {
134
+ throw databaseUrlError('the path must contain exactly one bucket name');
135
+ }
136
+ return {
137
+ bucketName: bucketName,
138
+ connectionString: "".concat(url.protocol, "//").concat(url.host).concat(url.search),
139
+ password: decodeDatabaseUrlComponent(url.password, 'the password'),
140
+ username: decodeDatabaseUrlComponent(url.username, 'the username'),
141
+ };
142
+ };
143
+ exports.parseDatabaseUrl = parseDatabaseUrl;
96
144
  var maskPassword = function (password) {
97
145
  return password ? 'xxxxxx' : 'empty';
98
146
  };
@@ -104,6 +152,7 @@ var splitStartOptions = function (options) {
104
152
  var getConnectionOptions = function (overrides) {
105
153
  if (overrides === void 0) { overrides = {}; }
106
154
  var proxy = envValue('COUCHBASE_PROXY');
155
+ var databaseUrl = envValue('DB_URL');
107
156
  var options = {
108
157
  bucketName: envValue('COUCHBASE_BUCKET', 'dev'),
109
158
  connectionString: envValue('COUCHBASE_URL', 'couchbase://localhost'),
@@ -113,7 +162,7 @@ var getConnectionOptions = function (overrides) {
113
162
  if (proxy) {
114
163
  options.proxy = proxy;
115
164
  }
116
- return __assign(__assign({}, options), overrides);
165
+ return __assign(__assign(__assign({}, options), (databaseUrl ? (0, exports.parseDatabaseUrl)(databaseUrl) : {})), overrides);
117
166
  };
118
167
  exports.getConnectionOptions = getConnectionOptions;
119
168
  exports.connectionOptions = (0, exports.getConnectionOptions)();
@@ -1 +1 @@
1
- {"version":3,"file":"database.js","sourceRoot":"","sources":["../src/database.ts"],"names":[],"mappings":"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","sourcesContent":["import CouchbaseConnection, {CouchsetArgs} from './connection';\n\nexport type CouchbaseStarterLogger = ((...args: any[]) => void) | false;\n\nexport interface StartCouchbaseOptions extends Partial<CouchsetArgs> {\n logger?: CouchbaseStarterLogger;\n}\n\nconst envValue = (key: string, fallback = ''): string => {\n const value = typeof process !== 'undefined' && process.env ? process.env[key] : undefined;\n\n return value === undefined ? fallback : value;\n};\n\nconst maskPassword = (password: string): string => {\n return password ? 'xxxxxx' : 'empty';\n};\n\nconst splitStartOptions = (options: StartCouchbaseOptions = {}) => {\n const {logger, ...connectionOverrides} = options;\n\n return {connectionOverrides, logger};\n};\n\nexport const getConnectionOptions = (overrides: Partial<CouchsetArgs> = {}): CouchsetArgs => {\n const proxy = envValue('COUCHBASE_PROXY');\n const options: CouchsetArgs = {\n bucketName: envValue('COUCHBASE_BUCKET', 'dev'),\n connectionString: envValue('COUCHBASE_URL', 'couchbase://localhost'),\n password: envValue('COUCHBASE_PASSWORD', '1234'),\n username: envValue('COUCHBASE_USERNAME', 'admin'),\n };\n\n if (proxy) {\n options.proxy = proxy;\n }\n\n return {\n ...options,\n ...overrides,\n };\n};\n\nexport const connectionOptions: CouchsetArgs = getConnectionOptions();\n\nconst refreshConnectionOptions = (overrides: Partial<CouchsetArgs> = {}): CouchsetArgs => {\n const nextOptions = getConnectionOptions(overrides);\n\n Object.keys(connectionOptions).forEach((key) => {\n delete (connectionOptions as any)[key];\n });\n Object.assign(connectionOptions, nextOptions);\n\n return connectionOptions;\n};\n\nconst logConnectionStart = (\n options: CouchsetArgs,\n logger: CouchbaseStarterLogger | undefined,\n mode: string\n): void => {\n if (logger === false) {\n return;\n }\n\n const log = logger || console.log;\n\n log(\n 'Couchbase',\n 'starting',\n mode,\n JSON.stringify({\n bucket: options.bucketName,\n host: options.connectionString,\n password: maskPassword(options.password),\n username: options.username,\n })\n );\n};\n\nconst logConnectionStarted = (\n options: CouchsetArgs,\n logger: CouchbaseStarterLogger | undefined,\n mode: string\n): void => {\n if (logger === false) {\n return;\n }\n\n const log = logger || console.log;\n\n log(\n 'Couchbase',\n 'started',\n mode,\n JSON.stringify({bucket: options.bucketName, host: options.connectionString})\n );\n};\n\nexport const startCouchbase = async (options: StartCouchbaseOptions = {}): Promise<boolean> => {\n const {connectionOverrides, logger} = splitStartOptions(options);\n const resolvedOptions = refreshConnectionOptions(connectionOverrides);\n\n logConnectionStart(resolvedOptions, logger, 'standard');\n await CouchbaseConnection.Instance.init(resolvedOptions);\n logConnectionStarted(resolvedOptions, logger, 'standard');\n\n return true;\n};\n\nexport const startCouchbaseServerless = async (\n options: StartCouchbaseOptions = {}\n): Promise<boolean> => {\n const {connectionOverrides, logger} = splitStartOptions(options);\n const resolvedOptions = refreshConnectionOptions(connectionOverrides);\n\n logConnectionStart(resolvedOptions, logger, 'serverless');\n await CouchbaseConnection.Instance.initServerless(resolvedOptions);\n logConnectionStarted(resolvedOptions, logger, 'serverless');\n\n return true;\n};\n"]}
1
+ {"version":3,"file":"database.js","sourceRoot":"","sources":["../src/database.ts"],"names":[],"mappings":"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","sourcesContent":["import CouchbaseConnection, {CouchsetArgs} from './connection';\n\nexport type CouchbaseStarterLogger = ((...args: any[]) => void) | false;\n\nexport interface StartCouchbaseOptions extends Partial<CouchsetArgs> {\n logger?: CouchbaseStarterLogger;\n}\n\nconst envValue = (key: string, fallback = ''): string => {\n const value = typeof process !== 'undefined' && process.env ? process.env[key] : undefined;\n\n return value === undefined ? fallback : value;\n};\n\nconst databaseUrlError = (reason: string): Error =>\n new Error(`DB_URL must use couchbase:// or couchbases://user:password@host/bucket: ${reason}`);\n\nconst decodeDatabaseUrlComponent = (value: string, label: string): string => {\n try {\n return decodeURIComponent(value);\n } catch (_error) {\n throw databaseUrlError(`${label} contains invalid percent-encoding`);\n }\n};\n\n/**\n * Convert a PostgreSQL-style DB_URL into Couchbase SDK connection settings.\n * The bucket lives in the URL path; it is not part of Couchbase's connection\n * string, which contains only the scheme, host, optional port, and options.\n */\nexport const parseDatabaseUrl = (\n value: string\n): Pick<CouchsetArgs, 'bucketName' | 'connectionString' | 'password' | 'username'> => {\n let url: URL;\n\n try {\n url = new URL(value);\n } catch (_error) {\n throw databaseUrlError('the value is not a valid URL');\n }\n\n if (url.protocol !== 'couchbase:' && url.protocol !== 'couchbases:') {\n throw databaseUrlError(`unsupported protocol ${url.protocol || '(missing)'}`);\n }\n if (!url.hostname) {\n throw databaseUrlError('a host is required');\n }\n if (!url.username || !url.password) {\n throw databaseUrlError('a username and password are required');\n }\n if (url.hash) {\n throw databaseUrlError('fragments are not supported');\n }\n\n const bucketName = decodeDatabaseUrlComponent(\n url.pathname.replace(/^\\//, ''),\n 'the bucket name'\n );\n if (!bucketName || bucketName.indexOf('/') !== -1) {\n throw databaseUrlError('the path must contain exactly one bucket name');\n }\n\n return {\n bucketName,\n connectionString: `${url.protocol}//${url.host}${url.search}`,\n password: decodeDatabaseUrlComponent(url.password, 'the password'),\n username: decodeDatabaseUrlComponent(url.username, 'the username'),\n };\n};\n\nconst maskPassword = (password: string): string => {\n return password ? 'xxxxxx' : 'empty';\n};\n\nconst splitStartOptions = (options: StartCouchbaseOptions = {}) => {\n const {logger, ...connectionOverrides} = options;\n\n return {connectionOverrides, logger};\n};\n\nexport const getConnectionOptions = (overrides: Partial<CouchsetArgs> = {}): CouchsetArgs => {\n const proxy = envValue('COUCHBASE_PROXY');\n const databaseUrl = envValue('DB_URL');\n const options: CouchsetArgs = {\n bucketName: envValue('COUCHBASE_BUCKET', 'dev'),\n connectionString: envValue('COUCHBASE_URL', 'couchbase://localhost'),\n password: envValue('COUCHBASE_PASSWORD', '1234'),\n username: envValue('COUCHBASE_USERNAME', 'admin'),\n };\n\n if (proxy) {\n options.proxy = proxy;\n }\n\n return {\n ...options,\n ...(databaseUrl ? parseDatabaseUrl(databaseUrl) : {}),\n ...overrides,\n };\n};\n\nexport const connectionOptions: CouchsetArgs = getConnectionOptions();\n\nconst refreshConnectionOptions = (overrides: Partial<CouchsetArgs> = {}): CouchsetArgs => {\n const nextOptions = getConnectionOptions(overrides);\n\n Object.keys(connectionOptions).forEach((key) => {\n delete (connectionOptions as any)[key];\n });\n Object.assign(connectionOptions, nextOptions);\n\n return connectionOptions;\n};\n\nconst logConnectionStart = (\n options: CouchsetArgs,\n logger: CouchbaseStarterLogger | undefined,\n mode: string\n): void => {\n if (logger === false) {\n return;\n }\n\n const log = logger || console.log;\n\n log(\n 'Couchbase',\n 'starting',\n mode,\n JSON.stringify({\n bucket: options.bucketName,\n host: options.connectionString,\n password: maskPassword(options.password),\n username: options.username,\n })\n );\n};\n\nconst logConnectionStarted = (\n options: CouchsetArgs,\n logger: CouchbaseStarterLogger | undefined,\n mode: string\n): void => {\n if (logger === false) {\n return;\n }\n\n const log = logger || console.log;\n\n log(\n 'Couchbase',\n 'started',\n mode,\n JSON.stringify({bucket: options.bucketName, host: options.connectionString})\n );\n};\n\nexport const startCouchbase = async (options: StartCouchbaseOptions = {}): Promise<boolean> => {\n const {connectionOverrides, logger} = splitStartOptions(options);\n const resolvedOptions = refreshConnectionOptions(connectionOverrides);\n\n logConnectionStart(resolvedOptions, logger, 'standard');\n await CouchbaseConnection.Instance.init(resolvedOptions);\n logConnectionStarted(resolvedOptions, logger, 'standard');\n\n return true;\n};\n\nexport const startCouchbaseServerless = async (\n options: StartCouchbaseOptions = {}\n): Promise<boolean> => {\n const {connectionOverrides, logger} = splitStartOptions(options);\n const resolvedOptions = refreshConnectionOptions(connectionOverrides);\n\n logConnectionStart(resolvedOptions, logger, 'serverless');\n await CouchbaseConnection.Instance.initServerless(resolvedOptions);\n logConnectionStarted(resolvedOptions, logger, 'serverless');\n\n return true;\n};\n"]}
@@ -2,22 +2,47 @@ import { LogicalWhereExpr, SortType } from '../query/interface/query.types';
2
2
  export type IncludeType = 'join' | 'leftJoin' | 'nest' | 'leftNest';
3
3
  export interface IncludeDefinition {
4
4
  as: string;
5
+ /** ANSI predicate; mutually exclusive with key/keys. */
6
+ on?: JoinPredicate;
7
+ /** Explicit trusted SQL++ escape hatch. Values still use positional placeholders. */
8
+ onRaw?: {
9
+ sql: string;
10
+ values?: readonly unknown[];
11
+ };
12
+ /** Top-level fields retained from the related document. */
13
+ select?: readonly string[];
5
14
  key?: string;
6
15
  keys?: string;
7
16
  keyspace?: string;
8
17
  model?: string | {
9
18
  keyspace: () => string;
19
+ parse?: <T>(data: T) => T;
20
+ parseProjection?: <T>(data: T) => T;
10
21
  };
11
22
  type?: IncludeType;
12
23
  optional?: boolean;
13
24
  }
25
+ /** A field operand is explicit; bare strings are always parameter values. */
26
+ export interface JoinField {
27
+ readonly $field: string;
28
+ }
29
+ export type JoinPredicate = {
30
+ $and: readonly JoinPredicate[];
31
+ } | {
32
+ $or: readonly JoinPredicate[];
33
+ } | {
34
+ left: JoinField;
35
+ op: '$eq' | '$neq' | '$gt' | '$gte' | '$lt' | '$lte';
36
+ right: unknown;
37
+ };
38
+ export declare const joinField: (path: string) => JoinField;
14
39
  export interface IncludedSelectionQueryArgs {
15
40
  keyspace: string;
16
41
  collectionName: string;
17
- select?: any[] | string;
42
+ select?: readonly any[] | string;
18
43
  where?: LogicalWhereExpr;
19
44
  orderBy?: Record<string, SortType>;
20
- include?: IncludeDefinition[];
45
+ include?: readonly IncludeDefinition[];
21
46
  limit?: number;
22
47
  page?: number;
23
48
  offset?: number;
@@ -29,5 +54,13 @@ export interface IncludedSelectionQuery {
29
54
  selectAll: boolean;
30
55
  resultKey?: string;
31
56
  }
32
- export declare const buildIncludeClauses: (keyspace: string, include?: IncludeDefinition[], sourceAlias?: string) => string;
57
+ declare class IncludeParameterStore {
58
+ parameters: Record<string, any>;
59
+ private index;
60
+ add(value: any): string;
61
+ set(key: string, value: any): string;
62
+ }
63
+ export declare const includeOperator: (include: IncludeDefinition) => string;
64
+ export declare const buildIncludeClauses: (keyspace: string, include?: readonly IncludeDefinition[], sourceAlias?: string, store?: IncludeParameterStore) => string;
33
65
  export declare const buildIncludedSelectionQuery: (args: IncludedSelectionQueryArgs) => IncludedSelectionQuery;
66
+ export {};
@@ -25,10 +25,16 @@ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
25
25
  return to.concat(ar || Array.prototype.slice.call(from));
26
26
  };
27
27
  Object.defineProperty(exports, "__esModule", { value: true });
28
- exports.buildIncludedSelectionQuery = exports.buildIncludeClauses = void 0;
29
- var helpers_1 = require("../query/helpers");
30
- var utils_1 = require("../query/utils");
28
+ exports.buildIncludedSelectionQuery = exports.buildIncludeClauses = exports.includeOperator = exports.joinField = void 0;
31
29
  var keyspace_1 = require("./keyspace");
30
+ var joinField = function (path) { return ({ $field: path }); };
31
+ exports.joinField = joinField;
32
+ var identifier = function (name) {
33
+ if (!name || name.indexOf('\0') !== -1)
34
+ throw new Error('Empty or invalid identifier');
35
+ return (0, keyspace_1.escapeIdentifier)(name);
36
+ };
37
+ var fieldPath = function (path) { return path.split('.').map(identifier).join('.'); };
32
38
  var comparisonOperators = {
33
39
  $eq: '=',
34
40
  $neq: '!=',
@@ -74,26 +80,30 @@ var IncludeParameterStore = /** @class */ (function () {
74
80
  };
75
81
  return IncludeParameterStore;
76
82
  }());
77
- var safeAlias = function (alias) {
78
- return /^[A-Za-z_][A-Za-z0-9_]*$/.test(alias) ? alias : (0, keyspace_1.escapeIdentifier)(alias);
83
+ var safeAlias = identifier;
84
+ var safeKeyspace = function (target) {
85
+ // Accept existing model.keyspace() output and simple dotted identifiers, never expressions.
86
+ var token = '(?:`(?:[^`]|``)+`|[A-Za-z_][A-Za-z0-9_-]*)';
87
+ if (!new RegExp("^(?:default:)?".concat(token, "(?:\\.").concat(token, "){0,2}$")).test(target))
88
+ throw new Error('Invalid include keyspace; use a model or identifier path');
89
+ return target.replace(/[A-Za-z_][A-Za-z0-9_-]*|`(?:[^`]|``)+`/g, function (part, offset) {
90
+ return target.slice(offset + part.length, offset + part.length + 1) === ':' || part.startsWith('`')
91
+ ? part
92
+ : identifier(part);
93
+ });
79
94
  };
80
95
  var keyspaceFromInclude = function (include, defaultKeyspace) {
81
- if (include.keyspace) {
82
- return include.keyspace;
83
- }
84
- if (typeof include.model === 'string') {
85
- return include.model;
86
- }
87
- if (include.model) {
88
- return include.model.keyspace();
89
- }
90
- return defaultKeyspace;
96
+ var _a;
97
+ var target = include.keyspace ||
98
+ (typeof include.model === 'string' ? include.model : (_a = include.model) === null || _a === void 0 ? void 0 : _a.keyspace()) ||
99
+ defaultKeyspace;
100
+ return safeKeyspace(target);
91
101
  };
92
102
  var sourceField = function (field, sourceAlias) {
93
- if (field.indexOf("".concat(sourceAlias, ".")) === 0) {
94
- return field;
95
- }
96
- return "".concat(sourceAlias, ".").concat((0, utils_1.escapeReservedWords)(field));
103
+ var prefix = "".concat(sourceAlias, ".");
104
+ return field.indexOf(prefix) === 0
105
+ ? fieldPath(field)
106
+ : "".concat(identifier(sourceAlias), ".").concat(fieldPath(field));
97
107
  };
98
108
  var includeOperator = function (include) {
99
109
  if (include.type === 'leftNest') {
@@ -105,22 +115,92 @@ var includeOperator = function (include) {
105
115
  if (include.type === 'leftJoin') {
106
116
  return 'LEFT JOIN';
107
117
  }
118
+ if (include.type === 'join') {
119
+ return include.optional ? 'LEFT JOIN' : 'JOIN';
120
+ }
108
121
  if (include.keys) {
109
122
  return include.optional ? 'LEFT NEST' : 'NEST';
110
123
  }
111
124
  return include.optional ? 'LEFT JOIN' : 'JOIN';
112
125
  };
113
- var buildIncludeClauses = function (keyspace, include, sourceAlias) {
126
+ exports.includeOperator = includeOperator;
127
+ var predicateExpr = function (predicate, store, aliases) {
128
+ if (!predicate || typeof predicate !== 'object')
129
+ throw new Error('Invalid ON predicate');
130
+ var keys = Object.keys(predicate);
131
+ if (keys.length === 1 && (keys[0] === '$and' || keys[0] === '$or')) {
132
+ var children = predicate[keys[0]];
133
+ if (!Array.isArray(children) || !children.length)
134
+ throw new Error('ON logical groups must be nonempty');
135
+ return "(".concat(children
136
+ .map(function (child) { return predicateExpr(child, store, aliases); })
137
+ .join(keys[0] === '$and' ? ' AND ' : ' OR '), ")");
138
+ }
139
+ var comparison = predicate;
140
+ if (keys.length !== 3 ||
141
+ !keys.includes('left') ||
142
+ !keys.includes('right') ||
143
+ !keys.includes('op') ||
144
+ !Object.prototype.hasOwnProperty.call(comparisonOperators, comparison.op))
145
+ throw new Error('Unsupported ON comparison');
146
+ var reference = function (value) {
147
+ if (!value || typeof value.$field !== 'string' || Object.keys(value).length !== 1)
148
+ throw new Error('Expected explicit joinField reference');
149
+ var parts = value.$field.split('.');
150
+ if (parts.length < 2 || !aliases.has(parts[0]))
151
+ throw new Error("Unknown ON alias: ".concat(parts[0]));
152
+ return fieldPath(value.$field);
153
+ };
154
+ var right = comparison.right;
155
+ return "".concat(reference(comparison.left), " ").concat(comparisonOperators[comparison.op], " ").concat(right && typeof right === 'object' && Object.prototype.hasOwnProperty.call(right, '$field')
156
+ ? reference(right)
157
+ : store.add(right));
158
+ };
159
+ var buildIncludeClauses = function (keyspace, include, sourceAlias, store) {
114
160
  if (include === void 0) { include = []; }
115
161
  if (sourceAlias === void 0) { sourceAlias = 'doc'; }
162
+ if (store === void 0) { store = new IncludeParameterStore(); }
163
+ var aliases = new Set([sourceAlias]);
164
+ var reservedAliases = new Set(['__cs_root', '__proto__', 'constructor', 'prototype']);
165
+ var modes = new Set(include.map(function (item) { return (item.on || item.onRaw ? 'ansi' : 'keys'); }));
166
+ // Couchbase does not permit mixing lookup and ANSI joins in the same FROM clause.
167
+ if (modes.size > 1)
168
+ throw new Error('Cannot mix ANSI ON and ON KEYS includes');
116
169
  return include
117
170
  .map(function (item) {
118
- var key = item.key || item.keys;
119
- if (!key) {
120
- throw new Error("include ".concat(item.as, " requires key or keys"));
171
+ if (aliases.has(item.as) || reservedAliases.has(item.as))
172
+ throw new Error("Duplicate or reserved include alias: ".concat(item.as));
173
+ aliases.add(item.as);
174
+ if (item.type && !['join', 'leftJoin', 'nest', 'leftNest'].includes(item.type))
175
+ throw new Error('Unsupported include type');
176
+ if ([
177
+ item.key !== undefined,
178
+ item.keys !== undefined,
179
+ item.on !== undefined,
180
+ item.onRaw !== undefined,
181
+ ].filter(Boolean).length !== 1) {
182
+ throw new Error("include ".concat(item.as, " requires exactly one of key, keys, on, onRaw"));
183
+ }
184
+ var condition;
185
+ if (item.on)
186
+ condition = "ON ".concat(predicateExpr(item.on, store, aliases));
187
+ else if (item.onRaw) {
188
+ var values_1 = item.onRaw.values || [];
189
+ var index_1 = 0;
190
+ if (!item.onRaw.sql.trim())
191
+ throw new Error('onRaw requires SQL');
192
+ var sql = item.onRaw.sql.replace(/\?/g, function () {
193
+ if (index_1 >= values_1.length)
194
+ throw new Error('onRaw placeholder/value mismatch');
195
+ return store.add(values_1[index_1++]);
196
+ });
197
+ if (index_1 !== values_1.length)
198
+ throw new Error('onRaw placeholder/value mismatch');
199
+ condition = "ON (".concat(sql, ")");
121
200
  }
122
- var alias = safeAlias(item.as);
123
- return "".concat(includeOperator(item), " ").concat(keyspaceFromInclude(item, keyspace), " AS ").concat(alias, " ON KEYS ").concat(sourceField(key, sourceAlias));
201
+ else
202
+ condition = "ON KEYS ".concat(sourceField((item.key || item.keys), sourceAlias));
203
+ return "".concat((0, exports.includeOperator)(item), " ").concat(keyspaceFromInclude(item, keyspace), " AS ").concat(safeAlias(item.as), " ").concat(condition);
124
204
  })
125
205
  .join(' ');
126
206
  };
@@ -201,46 +281,71 @@ var buildWhereExpr = function (expr, store, sourceAlias) {
201
281
  var buildOrderByExpr = function (orderBy, sourceAlias) {
202
282
  return orderBy
203
283
  ? " ORDER BY ".concat(Object.keys(orderBy)
204
- .map(function (field) { return "".concat(sourceField(field, sourceAlias), " ").concat(orderBy[field]); })
284
+ .map(function (field) {
285
+ if (!['ASC', 'DESC'].includes(orderBy[field]))
286
+ throw new Error('Unsupported order direction');
287
+ return "".concat(sourceField(field, sourceAlias), " ").concat(orderBy[field]);
288
+ })
205
289
  .join(','))
206
290
  : '';
207
291
  };
292
+ var selectedObject = function (alias, fields) {
293
+ if (!fields)
294
+ return identifier(alias);
295
+ if (!fields.length || new Set(fields).size !== fields.length)
296
+ throw new Error('Projection must contain distinct fields');
297
+ return "{".concat(fields
298
+ .map(function (field) {
299
+ if (field.includes('.'))
300
+ throw new Error('Include projections support top-level fields only');
301
+ return "".concat(JSON.stringify(field), ": ").concat(identifier(alias), ".").concat(identifier(field));
302
+ })
303
+ .join(', '), "}");
304
+ };
208
305
  var buildSelectExpr = function (select, include, sourceAlias) {
209
- if (!select || select === '*') {
210
- var aliases = include.map(function (item) { return safeAlias(item.as); });
211
- var mergeObject = aliases.map(function (alias) { return "".concat(JSON.stringify(alias), ": ").concat(alias); }).join(',');
306
+ var all = !select || select === '*';
307
+ var structured = Array.isArray(select) &&
308
+ select.every(function (field) { return typeof field === 'string' && !field.includes('.'); });
309
+ var root = all
310
+ ? identifier(sourceAlias)
311
+ : structured
312
+ ? selectedObject(sourceAlias, select)
313
+ : undefined;
314
+ var relations = include.map(function (item) {
315
+ var alias = identifier(item.as);
316
+ if (!item.select)
317
+ return "".concat(alias, " AS ").concat(alias);
318
+ var object = selectedObject('__cs_item', item.select);
319
+ var expression = (0, exports.includeOperator)(item).includes('NEST')
320
+ ? "ARRAY ".concat(object, " FOR ").concat(identifier('__cs_item'), " IN ").concat(alias, " END")
321
+ : "CASE WHEN ".concat(alias, " IS MISSING THEN MISSING WHEN ").concat(alias, " IS NULL THEN NULL ELSE ").concat(selectedObject(item.as, item.select), " END");
322
+ return "".concat(expression, " AS ").concat(alias);
323
+ });
324
+ if (structured &&
325
+ select.some(function (field) { return include.some(function (item) { return item.as === field; }); }))
326
+ throw new Error('Include alias conflicts with projected root field');
327
+ if (root)
212
328
  return {
213
- expr: "OBJECT_CONCAT(".concat(sourceAlias, ", {").concat(mergeObject, "}) AS ").concat(sourceAlias),
214
- selectAll: true,
215
- resultKey: sourceAlias,
329
+ expr: __spreadArray(["".concat(root, " AS __cs_root")], __read(relations), false).join(', '),
330
+ selectAll: all,
331
+ resultKey: '__cs_root',
216
332
  };
217
- }
218
- var includeSelect = include.map(function (item) {
219
- var alias = safeAlias(item.as);
220
- return "".concat(alias, " AS ").concat(alias);
221
- });
222
- if (typeof select === 'string') {
223
- return { expr: __spreadArray([select], __read(includeSelect), false).join(','), selectAll: false };
224
- }
225
- var modelSelect = select.map(function (item) {
226
- return typeof item === 'string'
227
- ? sourceField(item, sourceAlias)
228
- : (0, helpers_1.buildSelectArrayExpr)([item]);
229
- });
230
- return { expr: __spreadArray(__spreadArray([], __read(modelSelect), false), __read(includeSelect), false).join(','), selectAll: false };
333
+ throw new Error('Includes support only * or top-level field-array projections; use queryRows for arbitrary SQL++ projections');
231
334
  };
232
335
  var buildIncludedSelectionQuery = function (args) {
233
- var sourceAlias = safeAlias(args.sourceAlias || 'doc');
336
+ var sourceAlias = args.sourceAlias || 'doc';
337
+ if (sourceAlias === '__cs_root')
338
+ throw new Error('Reserved source alias');
234
339
  var include = args.include || [];
235
340
  var store = new IncludeParameterStore();
236
341
  var limit = typeof args.limit === 'number' ? args.limit : 10;
237
342
  var page = typeof args.page === 'number' ? args.page : 0;
238
343
  var offset = typeof args.offset === 'number' ? args.offset : page * limit;
239
344
  var _a = buildSelectExpr(args.select, include, sourceAlias), expr = _a.expr, selectAll = _a.selectAll, resultKey = _a.resultKey;
240
- var joins = (0, exports.buildIncludeClauses)(args.keyspace, include, sourceAlias);
345
+ var joins = (0, exports.buildIncludeClauses)(args.keyspace, include, sourceAlias, store);
241
346
  var limitExpr = " LIMIT ".concat(store.set('cs_limit', limit), " OFFSET ").concat(store.set('cs_offset', offset));
242
347
  return {
243
- query: "SELECT ".concat(expr, " FROM ").concat(args.keyspace, " AS ").concat(sourceAlias, " ").concat(joins).concat(buildWhereExpr(args.where, store, sourceAlias)).concat(buildOrderByExpr(args.orderBy, sourceAlias)).concat(limitExpr),
348
+ query: "SELECT ".concat(expr, " FROM ").concat(args.keyspace, " AS ").concat(identifier(sourceAlias), " ").concat(joins).concat(buildWhereExpr(args.where, store, sourceAlias)).concat(buildOrderByExpr(args.orderBy, sourceAlias)).concat(limitExpr),
244
349
  parameters: store.parameters,
245
350
  resultKey: resultKey,
246
351
  selectAll: selectAll,