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 +93 -1
- package/dist/database.d.ts +6 -0
- package/dist/database.js +51 -2
- package/dist/database.js.map +1 -1
- package/dist/model/include.d.ts +36 -3
- package/dist/model/include.js +154 -49
- package/dist/model/include.js.map +1 -1
- package/dist/model/index.d.ts +4 -1
- package/dist/model/index.js +8 -1
- package/dist/model/index.js.map +1 -1
- package/dist/model/read-helpers.d.ts +6 -2
- package/dist/model/read-helpers.js +96 -23
- package/dist/model/read-helpers.js.map +1 -1
- package/dist/model/read-types.d.ts +51 -0
- package/dist/model/read-types.js +3 -0
- package/dist/model/read-types.js.map +1 -0
- package/dist/model/safe-query.d.ts +3 -0
- package/dist/model/safe-query.js +22 -2
- package/dist/model/safe-query.js.map +1 -1
- package/dist/next.d.ts +12 -2
- package/dist/next.js +3 -1
- package/dist/next.js.map +1 -1
- package/dist/next.types.js +149 -1
- package/dist/next.types.js.map +1 -1
- package/dist/pagination/pagination.js +3 -1
- package/dist/pagination/pagination.js.map +1 -1
- package/dist/pagination/types.d.ts +2 -0
- package/dist/pagination/types.js.map +1 -1
- package/dist/search/customQuery.d.ts +2 -0
- package/dist/search/customQuery.js +4 -1
- package/dist/search/customQuery.js.map +1 -1
- package/docs/beta-migration.md +1 -1
- package/docs/document-modeling.md +5 -1
- package/docs/joins-and-consistency.md +228 -0
- package/docs/next-primitives.md +4 -0
- package/package.json +2 -1
package/dist/next.types.js
CHANGED
|
@@ -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
|
package/dist/next.types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"next.types.js","sourceRoot":"","sources":["../src/next.types.ts"],"names":[],"mappings":"
|
|
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,
|
|
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;
|
|
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":["
|
|
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 =
|
|
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;
|
|
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"]}
|
package/docs/beta-migration.md
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
package/docs/next-primitives.md
CHANGED
|
@@ -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.
|
|
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",
|