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.
@@ -1,6 +1,41 @@
1
1
  "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __generator = (this && this.__generator) || function (thisArg, body) {
12
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
13
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14
+ function verb(n) { return function (v) { return step([n, v]); }; }
15
+ function step(op) {
16
+ if (f) throw new TypeError("Generator is already executing.");
17
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
18
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
19
+ if (y = 0, t) op = [op[0] & 2, t.value];
20
+ switch (op[0]) {
21
+ case 0: case 1: t = op; break;
22
+ case 4: _.label++; return { value: op[1], done: false };
23
+ case 5: _.label++; y = op[1]; op = [0]; continue;
24
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
25
+ default:
26
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
27
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
28
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
29
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
30
+ if (t[2]) _.ops.pop();
31
+ _.trys.pop(); continue;
32
+ }
33
+ op = body.call(thisArg, _);
34
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
35
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36
+ }
37
+ };
2
38
  Object.defineProperty(exports, "__esModule", { value: true });
3
- /* eslint-disable @typescript-eslint/ban-ts-comment -- this file verifies compiler errors. */
4
39
  var next_1 = require("./next");
5
40
  var booleanCodec = {
6
41
  fromDatabase: function (value) { return value === 'true'; },
@@ -35,4 +70,117 @@ var booleanCodec = {
35
70
  ],
36
71
  name: 'InvalidTypedSession',
37
72
  });
73
+ // Model read inference is based on the bound model, literal aliases, kinds and projections.
74
+ var client = (0, next_1.createCouchsetClient)();
75
+ var sessions = client.model((0, next_1.defineModel)({ name: 'ReadSession', codecs: { expiresAt: next_1.dateCodec } }));
76
+ var profiles = client.model((0, next_1.defineModel)({
77
+ name: 'ReadProfile',
78
+ codecs: { birthday: next_1.dateCodec },
79
+ }));
80
+ function inferredReads() {
81
+ return __awaiter(this, void 0, void 0, function () {
82
+ var rows, expiry, birthday, friends, selected, selectedDate, raw, keyJoined, name, keyedNest, people, scoped, stillDate;
83
+ var _a;
84
+ return __generator(this, function (_b) {
85
+ switch (_b.label) {
86
+ case 0: return [4 /*yield*/, sessions.findMany({
87
+ include: [
88
+ {
89
+ as: 'creator',
90
+ model: profiles,
91
+ type: 'leftJoin',
92
+ on: { left: (0, next_1.joinField)('creator.id'), op: '$eq', right: (0, next_1.joinField)('doc.userId') },
93
+ },
94
+ {
95
+ as: 'friends',
96
+ model: profiles,
97
+ type: 'leftNest',
98
+ on: { left: (0, next_1.joinField)('friends.id'), op: '$eq', right: 'literal.id' },
99
+ select: ['name'],
100
+ },
101
+ ],
102
+ })];
103
+ case 1:
104
+ rows = _b.sent();
105
+ expiry = rows[0].expiresAt;
106
+ birthday = (_a = rows[0].creator) === null || _a === void 0 ? void 0 : _a.birthday;
107
+ friends = rows[0].friends;
108
+ // @ts-expect-error JOIN is an object, not a relation array.
109
+ rows[0].creator.map(function () { return 1; });
110
+ // @ts-expect-error Related projection excludes birthday.
111
+ rows[0].friends[0].birthday;
112
+ // @ts-expect-error Related fields come from its model.
113
+ rows[0].creator.nonexistent;
114
+ return [4 /*yield*/, sessions.findOne({ select: ['expiresAt'] })];
115
+ case 2:
116
+ selected = _b.sent();
117
+ selectedDate = selected.expiresAt;
118
+ // @ts-expect-error Unselected root field must not be claimed.
119
+ selected.userId;
120
+ // @ts-expect-error Cannot assert an unrelated result by supplying a row generic.
121
+ sessions.findMany();
122
+ // @ts-expect-error Include aliases must not overwrite known root fields.
123
+ sessions.findMany({ include: [{ as: 'userId', model: profiles, key: 'userId' }] });
124
+ return [4 /*yield*/, sessions.findMany({ select: 'RAW COUNT(1)' })];
125
+ case 3:
126
+ raw = _b.sent();
127
+ // @ts-expect-error Arbitrary SQL++ projections have unknown shape.
128
+ raw[0].expiresAt;
129
+ return [4 /*yield*/, sessions.findMany({
130
+ include: [{ as: 'profile', model: profiles, key: 'userId' }],
131
+ })];
132
+ case 4:
133
+ keyJoined = _b.sent();
134
+ name = keyJoined[0].profile.name;
135
+ return [4 /*yield*/, sessions.page({
136
+ include: [{ as: 'people', model: profiles, keys: 'userId' }],
137
+ })];
138
+ case 5:
139
+ keyedNest = _b.sent();
140
+ people = keyedNest.items[0].people;
141
+ return [4 /*yield*/, sessions.withDeleted().findOne()];
142
+ case 6:
143
+ scoped = _b.sent();
144
+ stillDate = scoped.expiresAt;
145
+ return [2 /*return*/, { expiry: expiry, birthday: birthday, friends: friends, selectedDate: selectedDate, name: name, people: people, stillDate: stillDate }];
146
+ }
147
+ });
148
+ });
149
+ }
150
+ function conservativeInputs(args, optional) {
151
+ return __awaiter(this, void 0, void 0, function () {
152
+ var dynamic, optionalRows, optionalKey, literalRows, leftKey, innerRows, innerKey;
153
+ return __generator(this, function (_a) {
154
+ switch (_a.label) {
155
+ case 0: return [4 /*yield*/, sessions.findMany(args)];
156
+ case 1:
157
+ dynamic = _a.sent();
158
+ // @ts-expect-error Broad read args can contain arbitrary projections.
159
+ dynamic[0].expiresAt;
160
+ return [4 /*yield*/, sessions.findMany({
161
+ include: [{ as: 'profile', model: profiles, key: 'userId', optional: optional }],
162
+ })];
163
+ case 2:
164
+ optionalRows = _a.sent();
165
+ optionalKey = true;
166
+ return [4 /*yield*/, sessions.findMany({
167
+ include: [{ as: 'profile', model: profiles, type: 'leftJoin', key: 'userId' }],
168
+ })];
169
+ case 3:
170
+ literalRows = _a.sent();
171
+ leftKey = true;
172
+ return [4 /*yield*/, sessions.findMany({
173
+ include: [{ as: 'profile', model: profiles, type: 'join', key: 'userId' }],
174
+ })];
175
+ case 4:
176
+ innerRows = _a.sent();
177
+ innerKey = false;
178
+ return [2 /*return*/, { optionalKey: optionalKey, leftKey: leftKey, innerKey: innerKey }];
179
+ }
180
+ });
181
+ });
182
+ }
183
+ // Keep compile-only cases referenced without executing reads.
184
+ void inferredReads;
185
+ void conservativeInputs;
38
186
  //# sourceMappingURL=next.types.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"next.types.js","sourceRoot":"","sources":["../src/next.types.ts"],"names":[],"mappings":";;AAAA,6FAA6F;AAC7F,+BAA0D;AAO1D,IAAM,YAAY,GAAgC;IAC9C,YAAY,EAAE,UAAC,KAAK,IAAK,OAAA,KAAK,KAAK,MAAM,EAAhB,CAAgB;IACzC,UAAU,EAAE,UAAC,KAAK,IAAK,OAAA,MAAM,CAAC,KAAK,CAAC,EAAb,CAAa;CACvC,CAAC;AAEF,IAAA,kBAAW,EAAe;IACtB,MAAM,EAAE,EAAC,SAAS,EAAE,gBAAS,EAAC;IAC9B,OAAO,EAAE,CAAC,EAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,wBAAwB,EAAC,CAAC;IAC/D,IAAI,EAAE,cAAc;CACvB,CAAC,CAAC;AAEH,IAAA,kBAAW,EAAe;IACtB,MAAM,EAAE;QACJ,wEAAwE;QACxE,OAAO,EAAE,gBAAS;KACrB;IACD,IAAI,EAAE,iBAAiB;CAC1B,CAAC,CAAC;AAEH,IAAA,kBAAW,EAAe;IACtB,MAAM,EAAE;QACJ,mEAAmE;QACnE,SAAS,EAAE,YAAY;KAC1B;IACD,IAAI,EAAE,mBAAmB;CAC5B,CAAC,CAAC;AAEH,IAAA,kBAAW,EAAe;IACtB,OAAO,EAAE;QACL;YACI,gFAAgF;YAChF,MAAM,EAAE,CAAC,SAAS,CAAC;YACnB,IAAI,EAAE,mBAAmB;SAC5B;KACJ;IACD,IAAI,EAAE,qBAAqB;CAC9B,CAAC,CAAC","sourcesContent":["/* eslint-disable @typescript-eslint/ban-ts-comment -- this file verifies compiler errors. */\nimport {dateCodec, defineModel, FieldCodec} from './next';\n\ninterface TypedSession {\n expiresAt: Date;\n userId: string;\n}\n\nconst booleanCodec: FieldCodec<boolean, string> = {\n fromDatabase: (value) => value === 'true',\n toDatabase: (value) => String(value),\n};\n\ndefineModel<TypedSession>({\n codecs: {expiresAt: dateCodec},\n indexes: [{fields: ['userId'], name: 'idx_typed_session_user'}],\n name: 'TypedSession',\n});\n\ndefineModel<TypedSession>({\n codecs: {\n // @ts-expect-error Codec keys must exist on the declared document type.\n missing: dateCodec,\n },\n name: 'InvalidCodecKey',\n});\n\ndefineModel<TypedSession>({\n codecs: {\n // @ts-expect-error Codec input must match the declared field type.\n expiresAt: booleanCodec,\n },\n name: 'InvalidCodecValue',\n});\n\ndefineModel<TypedSession>({\n indexes: [\n {\n // @ts-expect-error Declared index fields are checked against the document type.\n fields: ['missing'],\n name: 'idx_invalid_field',\n },\n ],\n name: 'InvalidTypedSession',\n});\n"]}
1
+ {"version":3,"file":"next.types.js","sourceRoot":"","sources":["../src/next.types.ts"],"names":[],"mappings":"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","sourcesContent":["/* eslint-disable @typescript-eslint/ban-ts-comment -- this file verifies compiler errors. */\nimport type {AutoModelFields, ModelReadArgs} from './model';\nimport {createCouchsetClient, dateCodec, defineModel, FieldCodec, joinField} from './next';\n\ninterface TypedSession {\n expiresAt: Date;\n userId: string;\n}\n\nconst booleanCodec: FieldCodec<boolean, string> = {\n fromDatabase: (value) => value === 'true',\n toDatabase: (value) => String(value),\n};\n\ndefineModel<TypedSession>({\n codecs: {expiresAt: dateCodec},\n indexes: [{fields: ['userId'], name: 'idx_typed_session_user'}],\n name: 'TypedSession',\n});\n\ndefineModel<TypedSession>({\n codecs: {\n // @ts-expect-error Codec keys must exist on the declared document type.\n missing: dateCodec,\n },\n name: 'InvalidCodecKey',\n});\n\ndefineModel<TypedSession>({\n codecs: {\n // @ts-expect-error Codec input must match the declared field type.\n expiresAt: booleanCodec,\n },\n name: 'InvalidCodecValue',\n});\n\ndefineModel<TypedSession>({\n indexes: [\n {\n // @ts-expect-error Declared index fields are checked against the document type.\n fields: ['missing'],\n name: 'idx_invalid_field',\n },\n ],\n name: 'InvalidTypedSession',\n});\n\n// Model read inference is based on the bound model, literal aliases, kinds and projections.\nconst client = createCouchsetClient();\nconst sessions = client.model(\n defineModel<TypedSession>({name: 'ReadSession', codecs: {expiresAt: dateCodec}})\n);\nconst profiles = client.model(\n defineModel<{name: string; birthday: Date}>({\n name: 'ReadProfile',\n codecs: {birthday: dateCodec},\n })\n);\nasync function inferredReads() {\n const rows = await sessions.findMany({\n include: [\n {\n as: 'creator',\n model: profiles,\n type: 'leftJoin',\n on: {left: joinField('creator.id'), op: '$eq', right: joinField('doc.userId')},\n },\n {\n as: 'friends',\n model: profiles,\n type: 'leftNest',\n on: {left: joinField('friends.id'), op: '$eq', right: 'literal.id'},\n select: ['name'],\n },\n ],\n });\n const expiry: Date = rows[0].expiresAt;\n const birthday: Date | undefined = rows[0].creator?.birthday;\n const friends: Array<{name: string}> = rows[0].friends;\n // @ts-expect-error JOIN is an object, not a relation array.\n rows[0].creator.map(() => 1);\n // @ts-expect-error Related projection excludes birthday.\n rows[0].friends[0].birthday;\n // @ts-expect-error Related fields come from its model.\n rows[0].creator.nonexistent;\n const selected = await sessions.findOne({select: ['expiresAt']});\n const selectedDate: Date = selected.expiresAt;\n // @ts-expect-error Unselected root field must not be claimed.\n selected.userId;\n // @ts-expect-error Cannot assert an unrelated result by supplying a row generic.\n sessions.findMany<{madeUp: string}>();\n // @ts-expect-error Include aliases must not overwrite known root fields.\n sessions.findMany({include: [{as: 'userId', model: profiles, key: 'userId'}]});\n const raw = await sessions.findMany({select: 'RAW COUNT(1)'});\n // @ts-expect-error Arbitrary SQL++ projections have unknown shape.\n raw[0].expiresAt;\n const keyJoined = await sessions.findMany({\n include: [{as: 'profile', model: profiles, key: 'userId'}],\n });\n const name: string = keyJoined[0].profile.name;\n const keyedNest = await sessions.page({\n include: [{as: 'people', model: profiles, keys: 'userId'}],\n });\n const people: Array<{name: string; birthday: Date} & AutoModelFields> =\n keyedNest.items[0].people;\n const scoped = await sessions.withDeleted().findOne();\n const stillDate: Date = scoped.expiresAt;\n return {expiry, birthday, friends, selectedDate, name, people, stillDate};\n}\n\nasync function conservativeInputs(args: ModelReadArgs, optional: boolean) {\n const dynamic = await sessions.findMany(args);\n // @ts-expect-error Broad read args can contain arbitrary projections.\n dynamic[0].expiresAt;\n const optionalRows = await sessions.findMany({\n include: [{as: 'profile', model: profiles, key: 'userId', optional}],\n });\n type OptionalKey = {} extends Pick<(typeof optionalRows)[number], 'profile'> ? true : false;\n const optionalKey: OptionalKey = true;\n const literalRows = await sessions.findMany({\n include: [{as: 'profile', model: profiles, type: 'leftJoin', key: 'userId'}],\n });\n type LeftKey = {} extends Pick<(typeof literalRows)[number], 'profile'> ? true : false;\n const leftKey: LeftKey = true;\n const innerRows = await sessions.findMany({\n include: [{as: 'profile', model: profiles, type: 'join', key: 'userId'}],\n });\n type InnerKey = {} extends Pick<(typeof innerRows)[number], 'profile'> ? true : false;\n const innerKey: InnerKey = false;\n return {optionalKey, leftKey, innerKey};\n}\n\n// Keep compile-only cases referenced without executing reads.\nvoid inferredReads;\nvoid conservativeInputs;\n"]}
@@ -41,6 +41,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
42
  exports.Pagination = void 0;
43
43
  var connection_1 = __importDefault(require("../connection"));
44
+ var safe_query_1 = require("../model/safe-query");
44
45
  var safe_pagination_1 = require("./safe-pagination");
45
46
  /**
46
47
  * Common pagination
@@ -59,11 +60,12 @@ var Pagination = function (args) { return __awaiter(void 0, void 0, void 0, func
59
60
  case 0:
60
61
  _a = args.bucketName, bucketName = _a === void 0 ? '_default' : _a, _b = args.resultKey, resultKey = _b === void 0 ? bucketName : _b;
61
62
  cluster = connection_1.default.Instance.cluster;
63
+ (0, safe_query_1.validateReadConsistency)(args.queryOptions);
62
64
  _d.label = 1;
63
65
  case 1:
64
66
  _d.trys.push([1, 3, , 4]);
65
67
  _c = (0, safe_pagination_1.buildPaginationQuery)(args), query = _c.query, parameters = _c.parameters, selectAll_1 = _c.selectAll;
66
- return [4 /*yield*/, cluster.query(query, { parameters: parameters })];
68
+ return [4 /*yield*/, cluster.query(query, (0, safe_query_1.queryOptionsWithParameters)(parameters, args.queryOptions))];
67
69
  case 2:
68
70
  rows = (_d.sent()).rows;
69
71
  completedRows = rows.map(function (r) {
@@ -1 +1 @@
1
- {"version":3,"file":"pagination.js","sourceRoot":"","sources":["../../src/pagination/pagination.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,6DAAgD;AAEhD,qDAAuD;AAGvD;;;;;;;;;GASG;AACI,IAAM,UAAU,GAAG,UAAO,IAAoB;;;;;gBAC1C,KAAmD,IAAI,WAAhC,EAAvB,UAAU,mBAAG,UAAU,KAAA,EAAE,KAA0B,IAAI,UAAR,EAAtB,SAAS,mBAAG,UAAU,KAAA,CAAS;gBAEzD,OAAO,GAAG,oBAAmB,CAAC,QAAQ,CAAC,OAAO,CAAC;;;;gBAG3C,KAAiC,IAAA,sCAAoB,EAAC,IAAI,CAAC,EAA1D,KAAK,WAAA,EAAE,UAAU,gBAAA,EAAE,0BAAS,CAA+B;gBAEnD,qBAAM,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,EAAC,UAAU,YAAA,EAAC,CAAC,EAAA;;gBAAhD,IAAI,GAAI,CAAA,SAAwC,CAAA,KAA5C;gBAEL,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,UAAC,CAAM;oBAClC,OAAO,WAAS,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;gBACxC,CAAC,CAAC,CAAC;gBAEH,sBAAO,aAAa,EAAC;;;gBAErB,IAAI,IAAI,CAAC,YAAY,KAAK,KAAK,EAAE,CAAC;oBAC9B,sBAAO,EAAE,EAAC;gBACd,CAAC;gBAED,MAAM,OAAK,CAAC;;;;KAEnB,CAAC;AAtBW,QAAA,UAAU,cAsBrB","sourcesContent":["import CouchbaseConnection from '../connection';\n\nimport {buildPaginationQuery} from './safe-pagination';\nimport {PaginationArgs} from './types';\n\n/**\n * Common pagination\n * where = {\n where: { owner: { $eq: \"stoqey\" }, _type: { $eq: \"Trade\" } },\n },\n page = 0,\n limit = 10,\n orderBy = { createdAt: \"DESC\" },\n * @param args PaginationArgs\n */\nexport const Pagination = async (args: PaginationArgs): Promise<any[]> => {\n const {bucketName = '_default', resultKey = bucketName} = args;\n\n const cluster = CouchbaseConnection.Instance.cluster;\n\n try {\n const {query, parameters, selectAll} = buildPaginationQuery(args);\n\n const {rows} = await cluster.query(query, {parameters});\n\n const completedRows = rows.map((r: any) => {\n return selectAll ? r[resultKey] : r;\n });\n\n return completedRows;\n } catch (error) {\n if (args.throwOnError === false) {\n return [];\n }\n\n throw error;\n }\n};\n"]}
1
+ {"version":3,"file":"pagination.js","sourceRoot":"","sources":["../../src/pagination/pagination.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,6DAAgD;AAChD,kDAAwF;AAExF,qDAAuD;AAGvD;;;;;;;;;GASG;AACI,IAAM,UAAU,GAAG,UAAO,IAAoB;;;;;gBAC1C,KAAmD,IAAI,WAAhC,EAAvB,UAAU,mBAAG,UAAU,KAAA,EAAE,KAA0B,IAAI,UAAR,EAAtB,SAAS,mBAAG,UAAU,KAAA,CAAS;gBAEzD,OAAO,GAAG,oBAAmB,CAAC,QAAQ,CAAC,OAAO,CAAC;gBAErD,IAAA,oCAAuB,EAAC,IAAI,CAAC,YAAY,CAAC,CAAC;;;;gBAEjC,KAAiC,IAAA,sCAAoB,EAAC,IAAI,CAAC,EAA1D,KAAK,WAAA,EAAE,UAAU,gBAAA,EAAE,0BAAS,CAA+B;gBAEnD,qBAAM,OAAO,CAAC,KAAK,CAC9B,KAAK,EACL,IAAA,uCAA0B,EAAC,UAAU,EAAE,IAAI,CAAC,YAAY,CAAC,CAC5D,EAAA;;gBAHM,IAAI,GAAI,CAAA,SAGd,CAAA,KAHU;gBAKL,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,UAAC,CAAM;oBAClC,OAAO,WAAS,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;gBACxC,CAAC,CAAC,CAAC;gBAEH,sBAAO,aAAa,EAAC;;;gBAErB,IAAI,IAAI,CAAC,YAAY,KAAK,KAAK,EAAE,CAAC;oBAC9B,sBAAO,EAAE,EAAC;gBACd,CAAC;gBAED,MAAM,OAAK,CAAC;;;;KAEnB,CAAC;AA1BW,QAAA,UAAU,cA0BrB","sourcesContent":["import CouchbaseConnection from '../connection';\nimport {queryOptionsWithParameters, validateReadConsistency} from '../model/safe-query';\n\nimport {buildPaginationQuery} from './safe-pagination';\nimport {PaginationArgs} from './types';\n\n/**\n * Common pagination\n * where = {\n where: { owner: { $eq: \"stoqey\" }, _type: { $eq: \"Trade\" } },\n },\n page = 0,\n limit = 10,\n orderBy = { createdAt: \"DESC\" },\n * @param args PaginationArgs\n */\nexport const Pagination = async (args: PaginationArgs): Promise<any[]> => {\n const {bucketName = '_default', resultKey = bucketName} = args;\n\n const cluster = CouchbaseConnection.Instance.cluster;\n\n validateReadConsistency(args.queryOptions);\n try {\n const {query, parameters, selectAll} = buildPaginationQuery(args);\n\n const {rows} = await cluster.query(\n query,\n queryOptionsWithParameters(parameters, args.queryOptions)\n );\n\n const completedRows = rows.map((r: any) => {\n return selectAll ? r[resultKey] : r;\n });\n\n return completedRows;\n } catch (error) {\n if (args.throwOnError === false) {\n return [];\n }\n\n throw error;\n }\n};\n"]}
@@ -1,3 +1,4 @@
1
+ import type { SafeQueryOptions } from '../model/safe-query';
1
2
  export interface PaginationArgs {
2
3
  bucketName: string;
3
4
  resultKey?: string;
@@ -8,4 +9,5 @@ export interface PaginationArgs {
8
9
  offset?: number;
9
10
  orderBy?: any;
10
11
  throwOnError?: boolean;
12
+ queryOptions?: SafeQueryOptions;
11
13
  }
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/pagination/types.ts"],"names":[],"mappings":"","sourcesContent":["export interface PaginationArgs {\n bucketName: string;\n resultKey?: string;\n select?: any[] | string;\n where: any;\n page?: number;\n limit?: number;\n offset?: number;\n orderBy?: any;\n throwOnError?: boolean;\n}\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/pagination/types.ts"],"names":[],"mappings":"","sourcesContent":["import type {SafeQueryOptions} from '../model/safe-query';\n\nexport interface PaginationArgs {\n bucketName: string;\n resultKey?: string;\n select?: any[] | string;\n where: any;\n page?: number;\n limit?: number;\n offset?: number;\n orderBy?: any;\n throwOnError?: boolean;\n queryOptions?: SafeQueryOptions;\n}\n"]}
@@ -1,3 +1,4 @@
1
+ import { SafeQueryOptions } from '../model/safe-query';
1
2
  export type CustomQueryParameters = {
2
3
  [key: string]: any;
3
4
  } | any[];
@@ -9,6 +10,7 @@ export interface CustomQueryArgs {
9
10
  debug?: boolean;
10
11
  logger?: CustomQueryLogger;
11
12
  throwOnError?: boolean;
13
+ queryOptions?: SafeQueryOptions;
12
14
  }
13
15
  export interface CustomQueryPagination {
14
16
  hasNext: boolean;
@@ -41,6 +41,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
42
  exports.CustomQuery = void 0;
43
43
  var connection_1 = __importDefault(require("../connection"));
44
+ var safe_query_1 = require("../model/safe-query");
44
45
  /**
45
46
  * Common pagination
46
47
  * query,
@@ -55,7 +56,9 @@ var CustomQuery = function (args) { return __awaiter(void 0, void 0, void 0, fun
55
56
  case 0:
56
57
  query = args.query, limit = args.limit, params = args.params, _a = args.debug, debug = _a === void 0 ? false : _a, logger = args.logger, _b = args.throwOnError, throwOnError = _b === void 0 ? true : _b;
57
58
  cluster = connection_1.default.Instance.cluster;
58
- queryOptions = params === undefined ? undefined : { parameters: params };
59
+ queryOptions = args.queryOptions || params !== undefined
60
+ ? (0, safe_query_1.queryOptionsWithParameters)(params, args.queryOptions)
61
+ : undefined;
59
62
  _d.label = 1;
60
63
  case 1:
61
64
  _d.trys.push([1, 3, , 4]);
@@ -1 +1 @@
1
- {"version":3,"file":"customQuery.js","sourceRoot":"","sources":["../../src/search/customQuery.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,6DAAgD;AAmBhD;;;;;;GAMG;AACI,IAAM,WAAW,GAAG,UACvB,IAAqB;;;;;gBAEd,KAAK,GAA+D,IAAI,MAAnE,EAAE,KAAK,GAAwD,IAAI,MAA5D,EAAE,MAAM,GAAgD,IAAI,OAApD,EAAE,KAA8C,IAAI,MAArC,EAAb,KAAK,mBAAG,KAAK,KAAA,EAAE,MAAM,GAAyB,IAAI,OAA7B,EAAE,KAAuB,IAAI,aAAR,EAAnB,YAAY,mBAAG,IAAI,KAAA,CAAS;gBAE1E,OAAO,GAAG,oBAAmB,CAAC,QAAQ,CAAC,OAAO,CAAC;gBAC/C,YAAY,GAAG,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAC,UAAU,EAAE,MAAM,EAAC,CAAC;;;;gBAGzE,IAAI,KAAK,IAAI,MAAM,EAAE,CAAC;oBAClB,MAAM,CAAC,sBAAsB,EAAE,EAAC,KAAK,OAAA,EAAE,OAAO,EAAE,YAAY,EAAC,CAAC,CAAC;gBACnE,CAAC;gBAEmB,qBAAM,OAAO,CAAC,KAAK,CAAI,KAAK,EAAE,YAAY,CAAC,EAAA;;gBAAxD,KAAa,CAAA,SAA2C,CAAA,KAA/C,EAAT,IAAI,mBAAG,EAAE,KAAA;gBAEV,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC;gBAEzB,OAAO,GAAG,UAAU,IAAI,KAAK,CAAC;gBAEpC,sBAAO,CAAC,IAAI,EAAE,EAAC,MAAM,QAAA,EAAE,OAAO,SAAA,EAAC,CAAC,EAAC;;;gBAEjC,IAAI,YAAY,EAAE,CAAC;oBACf,MAAM,OAAK,CAAC;gBAChB,CAAC;gBAED,sBAAO,CAAC,EAAE,EAAE,EAAC,MAAM,QAAA,EAAE,OAAO,EAAE,KAAK,EAAC,CAAC,EAAC;;;;KAE7C,CAAC;AA3BW,QAAA,WAAW,eA2BtB","sourcesContent":["import CouchbaseConnection from '../connection';\n\nexport type CustomQueryParameters = {[key: string]: any} | any[];\nexport type CustomQueryLogger = (message: string, details?: any) => void;\n\nexport interface CustomQueryArgs {\n query: string;\n limit: number;\n params?: CustomQueryParameters;\n debug?: boolean;\n logger?: CustomQueryLogger;\n throwOnError?: boolean;\n}\n\nexport interface CustomQueryPagination {\n hasNext: boolean;\n params?: CustomQueryParameters;\n}\n\n/**\n * Common pagination\n * query,\n bucketName = \"\",\n select = [\"id\", \"owner\"] || \"*\"\n * @param args PaginationArgs\n */\nexport const CustomQuery = async <T>(\n args: CustomQueryArgs\n): Promise<[T[], CustomQueryPagination]> => {\n const {query, limit, params, debug = false, logger, throwOnError = true} = args;\n\n const cluster = CouchbaseConnection.Instance.cluster;\n const queryOptions = params === undefined ? undefined : {parameters: params};\n\n try {\n if (debug && logger) {\n logger('couchset customQuery', {query, options: queryOptions});\n }\n\n const {rows = []} = await cluster.query<T>(query, queryOptions);\n\n const totalItems = rows.length;\n\n const hasNext = totalItems >= limit;\n\n return [rows, {params, hasNext}];\n } catch (error) {\n if (throwOnError) {\n throw error;\n }\n\n return [[], {params, hasNext: false}];\n }\n};\n"]}
1
+ {"version":3,"file":"customQuery.js","sourceRoot":"","sources":["../../src/search/customQuery.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,6DAAgD;AAChD,kDAAiF;AAoBjF;;;;;;GAMG;AACI,IAAM,WAAW,GAAG,UACvB,IAAqB;;;;;gBAEd,KAAK,GAA+D,IAAI,MAAnE,EAAE,KAAK,GAAwD,IAAI,MAA5D,EAAE,MAAM,GAAgD,IAAI,OAApD,EAAE,KAA8C,IAAI,MAArC,EAAb,KAAK,mBAAG,KAAK,KAAA,EAAE,MAAM,GAAyB,IAAI,OAA7B,EAAE,KAAuB,IAAI,aAAR,EAAnB,YAAY,mBAAG,IAAI,KAAA,CAAS;gBAE1E,OAAO,GAAG,oBAAmB,CAAC,QAAQ,CAAC,OAAO,CAAC;gBAC/C,YAAY,GACd,IAAI,CAAC,YAAY,IAAI,MAAM,KAAK,SAAS;oBACrC,CAAC,CAAC,IAAA,uCAA0B,EAAC,MAAM,EAAE,IAAI,CAAC,YAAY,CAAC;oBACvD,CAAC,CAAC,SAAS,CAAC;;;;gBAGhB,IAAI,KAAK,IAAI,MAAM,EAAE,CAAC;oBAClB,MAAM,CAAC,sBAAsB,EAAE,EAAC,KAAK,OAAA,EAAE,OAAO,EAAE,YAAY,EAAC,CAAC,CAAC;gBACnE,CAAC;gBAEmB,qBAAM,OAAO,CAAC,KAAK,CAAI,KAAK,EAAE,YAAY,CAAC,EAAA;;gBAAxD,KAAa,CAAA,SAA2C,CAAA,KAA/C,EAAT,IAAI,mBAAG,EAAE,KAAA;gBAEV,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC;gBAEzB,OAAO,GAAG,UAAU,IAAI,KAAK,CAAC;gBAEpC,sBAAO,CAAC,IAAI,EAAE,EAAC,MAAM,QAAA,EAAE,OAAO,SAAA,EAAC,CAAC,EAAC;;;gBAEjC,IAAI,YAAY,EAAE,CAAC;oBACf,MAAM,OAAK,CAAC;gBAChB,CAAC;gBAED,sBAAO,CAAC,EAAE,EAAE,EAAC,MAAM,QAAA,EAAE,OAAO,EAAE,KAAK,EAAC,CAAC,EAAC;;;;KAE7C,CAAC;AA9BW,QAAA,WAAW,eA8BtB","sourcesContent":["import CouchbaseConnection from '../connection';\nimport {queryOptionsWithParameters, SafeQueryOptions} from '../model/safe-query';\n\nexport type CustomQueryParameters = {[key: string]: any} | any[];\nexport type CustomQueryLogger = (message: string, details?: any) => void;\n\nexport interface CustomQueryArgs {\n query: string;\n limit: number;\n params?: CustomQueryParameters;\n debug?: boolean;\n logger?: CustomQueryLogger;\n throwOnError?: boolean;\n queryOptions?: SafeQueryOptions;\n}\n\nexport interface CustomQueryPagination {\n hasNext: boolean;\n params?: CustomQueryParameters;\n}\n\n/**\n * Common pagination\n * query,\n bucketName = \"\",\n select = [\"id\", \"owner\"] || \"*\"\n * @param args PaginationArgs\n */\nexport const CustomQuery = async <T>(\n args: CustomQueryArgs\n): Promise<[T[], CustomQueryPagination]> => {\n const {query, limit, params, debug = false, logger, throwOnError = true} = args;\n\n const cluster = CouchbaseConnection.Instance.cluster;\n const queryOptions =\n args.queryOptions || params !== undefined\n ? queryOptionsWithParameters(params, args.queryOptions)\n : undefined;\n\n try {\n if (debug && logger) {\n logger('couchset customQuery', {query, options: queryOptions});\n }\n\n const {rows = []} = await cluster.query<T>(query, queryOptions);\n\n const totalItems = rows.length;\n\n const hasNext = totalItems >= limit;\n\n return [rows, {params, hasNext}];\n } catch (error) {\n if (throwOnError) {\n throw error;\n }\n\n return [[], {params, hasNext: false}];\n }\n};\n"]}
@@ -78,7 +78,7 @@ await startCouchbase();
78
78
  await startCouchbaseServerless();
79
79
  ```
80
80
 
81
- The starters read `COUCHBASE_URL`, `COUCHBASE_BUCKET`, `COUCHBASE_USERNAME`, `COUCHBASE_PASSWORD`, and `COUCHBASE_PROXY`, and accept `CouchsetArgs` overrides.
81
+ The starters can instead read one `DB_URL` such as `couchbase://user:password@localhost/bucket` (or `couchbases://` for TLS). It takes precedence over `COUCHBASE_URL`, `COUCHBASE_BUCKET`, `COUCHBASE_USERNAME`, and `COUCHBASE_PASSWORD`; `COUCHBASE_PROXY` and explicit `CouchsetArgs` overrides remain supported.
82
82
 
83
83
  ## Query Behavior
84
84
 
@@ -99,7 +99,7 @@ CouchSet includes can join related documents by key, but large payload joins
99
99
  should be reserved for focused detail reads or small result sets.
100
100
 
101
101
  ```ts
102
- const report = await reports.findOne<ReportSummary & {payload: ReportPayload}>({
102
+ const report = await reports.findOne({
103
103
  where: {id: {$eq: reportId}},
104
104
  include: [
105
105
  {
@@ -112,6 +112,10 @@ const report = await reports.findOne<ReportSummary & {payload: ReportPayload}>({
112
112
  });
113
113
  ```
114
114
 
115
+ For client-bound models, the related type is inferred and `payload` may be absent
116
+ or null for an unmatched optional join. See [joined reads](joins-and-consistency.md)
117
+ for ANSI predicates, projection codecs, and alias requirements.
118
+
115
119
  For list pages, keep the companion model out of the query and rely on summary
116
120
  fields instead.
117
121
 
@@ -0,0 +1,228 @@
1
+ # Joined reads and query consistency
2
+
3
+ The 0.5.0 source already supported `JOIN`, `LEFT JOIN`, `NEST`, and `LEFT NEST`
4
+ with `ON KEYS`. This extends that include API with structured ANSI `ON`, related
5
+ model decoding, inferred client-model reads, and SDK query options. Raw
6
+ `queryRows`, `queryOne`, and `queryPage` already accepted SDK options; they now
7
+ share consistency validation with the other supported SQL++ reads.
8
+
9
+ ## ANSI predicates
10
+
11
+ ```ts
12
+ import {createCouchsetClient, defineModel, dateCodec, joinField} from 'couchset/next';
13
+ import {QueryScanConsistency} from 'couchbase';
14
+
15
+ const db = createCouchsetClient({bucketName: 'app' /* connection settings */});
16
+ const battles = db.model(defineModel<{
17
+ createdByUserId: string;
18
+ startsAt: Date;
19
+ }>({name: 'Battle', scope: 'app', collection: 'battles', codecs: {startsAt: dateCodec}}));
20
+ const profiles = db.model(defineModel<{
21
+ ownerUserId: string;
22
+ displayName: string;
23
+ birthday: Date;
24
+ }>({name: 'Profile', scope: 'app', collection: 'profiles', codecs: {birthday: dateCodec}}));
25
+
26
+ const result = await battles.page({
27
+ sourceAlias: 'battle',
28
+ include: [{
29
+ as: 'creator',
30
+ model: profiles,
31
+ type: 'leftJoin',
32
+ on: {
33
+ left: joinField('creator.ownerUserId'),
34
+ op: '$eq',
35
+ right: joinField('battle.createdByUserId'),
36
+ },
37
+ }],
38
+ limit: 20,
39
+ queryOptions: {scanConsistency: QueryScanConsistency.RequestPlus},
40
+ });
41
+ // LEFT JOIN ... profiles AS creator
42
+ // ON creator.ownerUserId = battle.createdByUserId
43
+ result.items[0]?.creator?.birthday; // Date | undefined
44
+ ```
45
+
46
+ A `joinField('alias.path')` is a field reference; an ordinary string on the
47
+ right is always a bound value, even when it contains a dot. Aliases must refer
48
+ to the root (default `doc`), the current include, or an earlier include. Field
49
+ paths use dots as separators and quote every identifier segment. Alias strings
50
+ are quoted, including reserved words. A literal dot inside a field name is not
51
+ supported by the structured field-reference syntax.
52
+
53
+ Comparisons support `$eq`, `$neq`, `$gt`, `$gte`, `$lt`, and `$lte`. Nest nonempty
54
+ `$and` and `$or` arrays to combine predicates:
55
+
56
+ ```ts
57
+ on: {$and: [
58
+ {left: joinField('creator.ownerUserId'), op: '$eq', right: joinField('battle.createdByUserId')},
59
+ {$or: [
60
+ {left: joinField('creator.displayName'), op: '$eq', right: 'A. Person'},
61
+ {left: joinField('creator.displayName'), op: '$eq', right: 'B. Person'},
62
+ ]},
63
+ ]}
64
+ ```
65
+
66
+ `type` accepts `join`, `leftJoin`, `nest`, or `leftNest`. `optional: true` turns
67
+ an inner kind into its left kind. Exactly one of `key`, `keys`, `on`, or `onRaw`
68
+ is required. `key`/`keys` remain source-document paths used with `ON KEYS`:
69
+
70
+ ```ts
71
+ include: [{as: 'creator', model: profiles, key: 'createdByUserId', optional: true}]
72
+ ```
73
+
74
+ Without an explicit type, `keys` means NEST and `key` means JOIN. An explicit
75
+ `type: 'join'` with `keys` now means JOIN, producing one row per matched key.
76
+ ANSI and lookup (`ON KEYS`) clauses cannot be mixed in one query. Unsupported
77
+ kinds, unknown aliases, empty groups, conflicting modes, duplicate aliases,
78
+ and keyspace expressions are rejected. Model keyspaces and quoted or simple
79
+ dotted keyspace identifiers are accepted.
80
+
81
+ For advanced predicates, `onRaw: {sql: 'META(`creator`).id = ?', values: [id]}`
82
+ is an explicit trusted-SQL escape hatch. Every `?` is replaced, left to right,
83
+ by a generated SDK parameter; do not put `?` inside SQL literals or comments.
84
+ Only use application-owned SQL in `sql`. Use `queryRows` for arbitrary
85
+ projections, UNNEST, subqueries, or other SQL++ beyond the structured API.
86
+
87
+ The related model supplies its keyspace, declared result type, and codecs.
88
+ It does not automatically apply that model's `_type`, default filters, or
89
+ soft-delete policy. Add required related predicates to `on` (especially when
90
+ several document types share one keyspace), or use explicit SQL for key-based
91
+ reads that need additional predicates. Definitions describe expected data;
92
+ read inference is not runtime schema validation or referential integrity.
93
+
94
+ Couchbase requires suitable indexes for ANSI joins. CouchSet does not create
95
+ them implicitly. See the official [JOIN reference](https://docs.couchbase.com/server/current/n1ql/n1ql-language-reference/join.html)
96
+ for supported chaining and indexing requirements.
97
+
98
+ ## Result shapes, projections, and codecs
99
+
100
+ Client-bound models infer reads from their definition. Literal include aliases
101
+ and bound related models determine the relation types; no row generic is
102
+ needed. In a separately stored options object, use `as const` to retain literal
103
+ aliases/kinds/selections. Broad or raw selections return `unknown`; dynamic
104
+ include arrays expose unknown relation values. Widened field arrays return
105
+ partial field selections; only tuples of definite individual literal keys
106
+ guarantee each selected field. Union-valued tuple entries also produce partial
107
+ selections. Union or dynamic alias names do not guarantee any particular alias.
108
+ Optional include settings retain their possible runtime defaults in the result type. Untyped keyspace targets have
109
+ unknown document fields. TypeScript 5 or newer is required for literal inference.
110
+
111
+ | Include kind | Relation value per returned row |
112
+ | --- | --- |
113
+ | JOIN | One related document object |
114
+ | LEFT JOIN | Related object, explicit `null`, or an absent property (`undefined` when accessed) |
115
+ | NEST | Array of matching related documents |
116
+ | LEFT NEST | Array; unmatched absent/null array values normalize to `[]` |
117
+
118
+ Inner clauses remove unmatched source rows on the server. CouchSet does not
119
+ invent those rows. JOIN cardinality is preserved, including repeated root IDs.
120
+ `page()` takes `limit + 1` result rows, not distinct root documents. NEST arrays
121
+ are decoded in the order received: neither source-key ordering nor any other
122
+ nested ordering is promised. Stable pagination needs an appropriate explicit
123
+ `orderBy`; it does not provide a snapshot across requests. See Couchbase's
124
+ [NEST reference](https://docs.couchbase.com/server/current/n1ql/n1ql-language-reference/nest.html).
125
+
126
+ Supported joined projections are `'*'` (the default) or arrays of distinct
127
+ top-level root fields, plus an optional `select` array on each include:
128
+
129
+ ```ts
130
+ const rows = await battles.findMany({
131
+ select: ['startsAt'],
132
+ include: [{as: 'creators', model: profiles, type: 'leftNest',
133
+ on: {left: joinField('creators.ownerUserId'), op: '$eq', right: joinField('doc.createdByUserId')},
134
+ select: ['displayName', 'birthday'],
135
+ }],
136
+ });
137
+ // rows: {startsAt: Date; creators: {displayName: string; birthday: Date}[]}[]
138
+ ```
139
+
140
+ Projections happen in SQL++, before rows reach the client. Present selected
141
+ fields run the model's codecs, schema date transforms, and `dateFields`.
142
+ Full-document parse hooks run only for full documents; projected documents
143
+ skip these hooks because omitted fields may be required by application code.
144
+ This applies independently to root and related documents. Full root decoding
145
+ is preserved. Raw/computed non-joined projections are returned unchanged and
146
+ do not run model codecs. Use an explicit decoder after `queryRows` for renamed
147
+ fields or computed expressions. Nested/dotted joined selections are not supported.
148
+
149
+ Root and relation columns are kept separate internally and combined after
150
+ decoding. An include alias must not overwrite a root field or another alias;
151
+ known root conflicts are type errors, and actual returned-field conflicts are
152
+ runtime errors. Choose `creator` alongside a source `creatorId`, not `creator`
153
+ as both the source key field and the populated result. Internal alias
154
+ `__cs_root` and prototype-related include aliases are reserved.
155
+
156
+ ## Read consistency
157
+
158
+ Set SDK options through `queryOptions` on `findMany`, `findOne`, `page`, `count`,
159
+ `exists`, standalone `Pagination`, and `CustomQuery`. They are forwarded on each
160
+ query, including joined reads. For `queryRows`, `queryOne`, and `queryPage`, use
161
+ the existing third options argument. Defaults remain unchanged: no consistency
162
+ mode is added by CouchSet. `count` and `exists` support root reads only and reject
163
+ includes; use an explicit aggregate `queryRows` query to count joined rows.
164
+
165
+ ```ts
166
+ import {MutationState, QueryScanConsistency} from 'couchbase';
167
+
168
+ await battles.findMany({
169
+ queryOptions: {scanConsistency: QueryScanConsistency.RequestPlus},
170
+ });
171
+
172
+ // Capture the SDK mutation result from a successful KV operation.
173
+ const mutation = await profiles.getCollection().upsert(profileId, storedProfile);
174
+ if (!mutation.token) throw new Error('Mutation token unavailable');
175
+ const state = new MutationState(mutation.token);
176
+ await battles.findMany({queryOptions: {consistentWith: state}});
177
+ await battles.queryRows('SELECT RAW 1', {}, {consistentWith: state});
178
+ ```
179
+
180
+ `request_plus` waits for indexes to catch up to mutations preceding the query.
181
+ `consistentWith` requests read-your-writes for the supplied SDK mutation state.
182
+ Neither option is a multi-document transaction or a stable snapshot across
183
+ multiple reads. KV reads and transaction-attempt queries keep their SDK-specific
184
+ APIs. See [SDK consistency options](https://docs.couchbase.com/nodejs-sdk/current/howtos/n1ql-queries-with-sdk.html).
185
+
186
+ `scanConsistency` and `consistentWith` cannot be combined, including an explicit
187
+ `not_bounded` mode. Unsupported scan modes and raw consistency overrides are
188
+ rejected before querying, even with `throwOnError: false`. SDK failures still
189
+ follow the existing fallback policy. SDK parameters remain separate from SQL.
190
+
191
+ ## Compatibility and validation
192
+
193
+ - Existing `ON KEYS` inputs remain available. Explicit JOIN with `keys` now honors
194
+ JOIN cardinality; omit the type or use NEST to retain the previous array shape.
195
+ - Client-bound `TypedModel` reads replace caller row generics with inference.
196
+ `new Model()` retains its existing generic read API for compatibility. For
197
+ arbitrary SQL++ result assertions, use the explicitly typed `queryRows<T>` API.
198
+ - Includes no longer overwrite root fields. Change colliding aliases. The SDK
199
+ row envelope has changed internally; public flat root-plus-relations results
200
+ remain, and mocked query responses must use the new separate columns.
201
+ - Arbitrary joined selections now fail explicitly; move them to `queryRows`.
202
+ Partial projections skip full-document hooks; raw projections skip decoding.
203
+ - Aggregate helpers reject includes they previously ignored. Conflicting
204
+ consistency options now fail early instead of relying on SDK behavior.
205
+
206
+ Run `npm run test:unit` for deterministic mocked SQL/binding, decoding, shape,
207
+ cardinality, and option-forwarding tests. Run `npm run test:types` for a build
208
+ and strict public-declaration inference fixtures. These tests do not execute a
209
+ Couchbase server, prove an index plan, or measure freshness under concurrency.
210
+
211
+
212
+ ### Local server validation
213
+
214
+ On 2026-09-05, 20 live probes passed against the configured local Couchbase
215
+ Query 8.0.3 server. The probes used a fresh isolated scope, synthetic documents,
216
+ and dedicated primary/lookup indexes; that scope and all its contents were
217
+ removed afterward. They exercised all ANSI and ON KEYS join kinds, duplicate
218
+ JOIN rows, compound predicates and binding, missing LEFT JOIN relations, empty
219
+ LEFT NEST arrays, root/related date codecs and projections, raw ON, pagination,
220
+ alias rejection, immediate `request_plus` reads, and normal/joined reads using
221
+ real SDK mutation tokens via `consistentWith`.
222
+
223
+ The first run exposed an example error: `MutationState` takes `mutation.token`,
224
+ not the whole mutation result. The example is corrected and covered by a strict
225
+ type fixture; the corrected live run passed all 20 probes. These observations
226
+ validate this local server and synthetic workload; they do not establish a
227
+ concurrent multi-document snapshot, a production index plan, or compatibility
228
+ with every supported server version.
@@ -24,6 +24,10 @@ const db = createCouchsetClient({
24
24
 
25
25
  `defineModel()` only describes metadata; `db.model(sessions)` only binds and registers the model. Neither operation creates Couchbase resources. Legacy `new Model()` remains supported and continues to use the existing singleton.
26
26
 
27
+ See [joined reads and consistency](joins-and-consistency.md) for inferred `findMany`,
28
+ `findOne`, and `page` results, structured ANSI predicates, relation codecs,
29
+ projections, and SDK read consistency options.
30
+
27
31
  ## Provisioning and dynamic models
28
32
 
29
33
  Run administrative DDL explicitly, with credentials that have `Manage Scopes` and query-index permissions:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "couchset",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Couchbase ORM",
5
5
  "private": false,
6
6
  "main": "dist/legacy/index.js",
@@ -60,6 +60,7 @@
60
60
  "lint": "tslint \"src/**/*.ts\" --project tsconfig.json",
61
61
  "build": "rimraf dist && ./node_modules/.bin/tsc --skipLibCheck",
62
62
  "test": "npm run test:unit && npm run test:integration",
63
+ "test:types": "npm run build && node scripts/check-read-types.js",
63
64
  "test:unit": "mocha \"src/**/*.test.ts\" --ignore src/index.test.ts --ignore src/index.serverless.test.ts --exit",
64
65
  "test:integration": "mocha src/index.test.ts --exit",
65
66
  "test:serverless": "mocha src/index.serverless.test.ts --exit",