couchset 0.5.0 → 0.5.2

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.
Files changed (39) hide show
  1. package/README.md +93 -1
  2. package/dist/database.d.ts +6 -0
  3. package/dist/database.js +51 -2
  4. package/dist/database.js.map +1 -1
  5. package/dist/model/field-path.d.ts +8 -0
  6. package/dist/model/field-path.js +70 -0
  7. package/dist/model/field-path.js.map +1 -0
  8. package/dist/model/include.d.ts +36 -3
  9. package/dist/model/include.js +155 -49
  10. package/dist/model/include.js.map +1 -1
  11. package/dist/model/index.d.ts +4 -1
  12. package/dist/model/index.js +8 -1
  13. package/dist/model/index.js.map +1 -1
  14. package/dist/model/read-helpers.d.ts +6 -2
  15. package/dist/model/read-helpers.js +96 -23
  16. package/dist/model/read-helpers.js.map +1 -1
  17. package/dist/model/read-types.d.ts +51 -0
  18. package/dist/model/read-types.js +3 -0
  19. package/dist/model/read-types.js.map +1 -0
  20. package/dist/model/safe-query.d.ts +3 -0
  21. package/dist/model/safe-query.js +22 -2
  22. package/dist/model/safe-query.js.map +1 -1
  23. package/dist/next.d.ts +12 -2
  24. package/dist/next.js +3 -1
  25. package/dist/next.js.map +1 -1
  26. package/dist/next.types.js +149 -1
  27. package/dist/next.types.js.map +1 -1
  28. package/dist/pagination/pagination.js +3 -1
  29. package/dist/pagination/pagination.js.map +1 -1
  30. package/dist/pagination/types.d.ts +2 -0
  31. package/dist/pagination/types.js.map +1 -1
  32. package/dist/search/customQuery.d.ts +2 -0
  33. package/dist/search/customQuery.js +4 -1
  34. package/dist/search/customQuery.js.map +1 -1
  35. package/docs/beta-migration.md +1 -1
  36. package/docs/document-modeling.md +5 -1
  37. package/docs/joins-and-consistency.md +266 -0
  38. package/docs/next-primitives.md +4 -0
  39. package/package.json +2 -1
@@ -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,266 @@
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. Nonnegative
50
+ integer indexes (`[0]`, `[1]`) and wildcard traversal (`[*]`) remain SQL++ array
51
+ operators. This parsing applies to `key`/`keys`, joined WHERE/orderBy fields,
52
+ and `joinField` references, including source-qualified paths. Alias strings
53
+ are quoted, including reserved words. Backtick-delimited segments represent
54
+ literal field names; double a backtick inside such a segment to escape it.
55
+ For example, the path string '`media[*]`' names a literal field, while
56
+ 'media[*].mediaId' traverses the media array. Empty segments, unmatched brackets,
57
+ negative/decimal indexes, slices, expression indexes, and unsafe-size indexes
58
+ are rejected. Top-level projection arrays remain literal field selections;
59
+ this change does not add array-expression projections or a fluent path builder.
60
+
61
+ Comparisons support `$eq`, `$neq`, `$gt`, `$gte`, `$lt`, and `$lte`. Nest nonempty
62
+ `$and` and `$or` arrays to combine predicates:
63
+
64
+ ```ts
65
+ on: {$and: [
66
+ {left: joinField('creator.ownerUserId'), op: '$eq', right: joinField('battle.createdByUserId')},
67
+ {$or: [
68
+ {left: joinField('creator.displayName'), op: '$eq', right: 'A. Person'},
69
+ {left: joinField('creator.displayName'), op: '$eq', right: 'B. Person'},
70
+ ]},
71
+ ]}
72
+ ```
73
+
74
+ `type` accepts `join`, `leftJoin`, `nest`, or `leftNest`. `optional: true` turns
75
+ an inner kind into its left kind. Exactly one of `key`, `keys`, `on`, or `onRaw`
76
+ is required. `key`/`keys` remain source-document paths used with `ON KEYS`:
77
+
78
+ ```ts
79
+ include: [{as: 'creator', model: profiles, key: 'createdByUserId', optional: true}]
80
+ ```
81
+
82
+ Without an explicit type, `keys` means NEST and `key` means JOIN. An explicit
83
+ `type: 'join'` with `keys` now means JOIN, producing one row per matched key.
84
+ ANSI and lookup (`ON KEYS`) clauses cannot be mixed in one query. Unsupported
85
+ kinds, unknown aliases, empty groups, conflicting modes, duplicate aliases,
86
+ and keyspace expressions are rejected. Model keyspaces and quoted or simple
87
+ dotted keyspace identifiers are accepted.
88
+
89
+ For advanced predicates, `onRaw: {sql: 'META(`creator`).id = ?', values: [id]}`
90
+ is an explicit trusted-SQL escape hatch. Every `?` is replaced, left to right,
91
+ by a generated SDK parameter; do not put `?` inside SQL literals or comments.
92
+ Only use application-owned SQL in `sql`. Use `queryRows` for arbitrary
93
+ projections, UNNEST, subqueries, or other SQL++ beyond the structured API.
94
+
95
+ The related model supplies its keyspace, declared result type, and codecs.
96
+ It does not automatically apply that model's `_type`, default filters, or
97
+ soft-delete policy. Add required related predicates to `on` (especially when
98
+ several document types share one keyspace), or use explicit SQL for key-based
99
+ reads that need additional predicates. Definitions describe expected data;
100
+ read inference is not runtime schema validation or referential integrity.
101
+
102
+ Couchbase requires suitable indexes for ANSI joins. CouchSet does not create
103
+ them implicitly. See the official [JOIN reference](https://docs.couchbase.com/server/current/n1ql/n1ql-language-reference/join.html)
104
+ for supported chaining and indexing requirements.
105
+
106
+ ## Result shapes, projections, and codecs
107
+
108
+ Client-bound models infer reads from their definition. Literal include aliases
109
+ and bound related models determine the relation types; no row generic is
110
+ needed. In a separately stored options object, use `as const` to retain literal
111
+ aliases/kinds/selections. Broad or raw selections return `unknown`; dynamic
112
+ include arrays expose unknown relation values. Widened field arrays return
113
+ partial field selections; only tuples of definite individual literal keys
114
+ guarantee each selected field. Union-valued tuple entries also produce partial
115
+ selections. Union or dynamic alias names do not guarantee any particular alias.
116
+ Optional include settings retain their possible runtime defaults in the result type. Untyped keyspace targets have
117
+ unknown document fields. TypeScript 5 or newer is required for literal inference.
118
+
119
+ | Include kind | Relation value per returned row |
120
+ | --- | --- |
121
+ | JOIN | One related document object |
122
+ | LEFT JOIN | Related object, explicit `null`, or an absent property (`undefined` when accessed) |
123
+ | NEST | Array of matching related documents |
124
+ | LEFT NEST | Array; unmatched absent/null array values normalize to `[]` |
125
+
126
+ Inner clauses remove unmatched source rows on the server. CouchSet does not
127
+ invent those rows. JOIN cardinality is preserved, including repeated root IDs.
128
+ `page()` takes `limit + 1` result rows, not distinct root documents. NEST arrays
129
+ are decoded in the order received: neither source-key ordering nor any other
130
+ nested ordering is promised. Stable pagination needs an appropriate explicit
131
+ `orderBy`; it does not provide a snapshot across requests. See Couchbase's
132
+ [NEST reference](https://docs.couchbase.com/server/current/n1ql/n1ql-language-reference/nest.html).
133
+
134
+ Supported joined projections are `'*'` (the default) or arrays of distinct
135
+ top-level root fields, plus an optional `select` array on each include:
136
+
137
+ ```ts
138
+ const rows = await battles.findMany({
139
+ select: ['startsAt'],
140
+ include: [{as: 'creators', model: profiles, type: 'leftNest',
141
+ on: {left: joinField('creators.ownerUserId'), op: '$eq', right: joinField('doc.createdByUserId')},
142
+ select: ['displayName', 'birthday'],
143
+ }],
144
+ });
145
+ // rows: {startsAt: Date; creators: {displayName: string; birthday: Date}[]}[]
146
+ ```
147
+
148
+ Projections happen in SQL++, before rows reach the client. Present selected
149
+ fields run the model's codecs, schema date transforms, and `dateFields`.
150
+ Full-document parse hooks run only for full documents; projected documents
151
+ skip these hooks because omitted fields may be required by application code.
152
+ This applies independently to root and related documents. Full root decoding
153
+ is preserved. Raw/computed non-joined projections are returned unchanged and
154
+ do not run model codecs. Use an explicit decoder after `queryRows` for renamed
155
+ fields or computed expressions. Nested/dotted joined selections are not supported.
156
+
157
+ Root and relation columns are kept separate internally and combined after
158
+ decoding. An include alias must not overwrite a root field or another alias;
159
+ known root conflicts are type errors, and actual returned-field conflicts are
160
+ runtime errors. Choose `creator` alongside a source `creatorId`, not `creator`
161
+ as both the source key field and the populated result. Internal alias
162
+ `__cs_root` and prototype-related include aliases are reserved.
163
+
164
+ ## Read consistency
165
+
166
+ Set SDK options through `queryOptions` on `findMany`, `findOne`, `page`, `count`,
167
+ `exists`, standalone `Pagination`, and `CustomQuery`. They are forwarded on each
168
+ query, including joined reads. For `queryRows`, `queryOne`, and `queryPage`, use
169
+ the existing third options argument. Defaults remain unchanged: no consistency
170
+ mode is added by CouchSet. `count` and `exists` support root reads only and reject
171
+ includes; use an explicit aggregate `queryRows` query to count joined rows.
172
+
173
+ ```ts
174
+ import {MutationState, QueryScanConsistency} from 'couchbase';
175
+
176
+ await battles.findMany({
177
+ queryOptions: {scanConsistency: QueryScanConsistency.RequestPlus},
178
+ });
179
+
180
+ // Capture the SDK mutation result from a successful KV operation.
181
+ const mutation = await profiles.getCollection().upsert(profileId, storedProfile);
182
+ if (!mutation.token) throw new Error('Mutation token unavailable');
183
+ const state = new MutationState(mutation.token);
184
+ await battles.findMany({queryOptions: {consistentWith: state}});
185
+ await battles.queryRows('SELECT RAW 1', {}, {consistentWith: state});
186
+ ```
187
+
188
+ `request_plus` waits for indexes to catch up to mutations preceding the query.
189
+ `consistentWith` requests read-your-writes for the supplied SDK mutation state.
190
+ Neither option is a multi-document transaction or a stable snapshot across
191
+ multiple reads. KV reads and transaction-attempt queries keep their SDK-specific
192
+ APIs. See [SDK consistency options](https://docs.couchbase.com/nodejs-sdk/current/howtos/n1ql-queries-with-sdk.html).
193
+
194
+ `scanConsistency` and `consistentWith` cannot be combined, including an explicit
195
+ `not_bounded` mode. Unsupported scan modes and raw consistency overrides are
196
+ rejected before querying, even with `throwOnError: false`. SDK failures still
197
+ follow the existing fallback policy. SDK parameters remain separate from SQL.
198
+
199
+ ## Compatibility and validation
200
+
201
+ - Existing `ON KEYS` inputs remain available. Explicit JOIN with `keys` now honors
202
+ JOIN cardinality; omit the type or use NEST to retain the previous array shape.
203
+ - Client-bound `TypedModel` reads replace caller row generics with inference.
204
+ `new Model()` retains its existing generic read API for compatibility. For
205
+ arbitrary SQL++ result assertions, use the explicitly typed `queryRows<T>` API.
206
+ - Includes no longer overwrite root fields. Change colliding aliases. The SDK
207
+ row envelope has changed internally; public flat root-plus-relations results
208
+ remain, and mocked query responses must use the new separate columns.
209
+ - Arbitrary joined selections now fail explicitly; move them to `queryRows`.
210
+ Partial projections skip full-document hooks; raw projections skip decoding.
211
+ - Aggregate helpers reject includes they previously ignored. Conflicting
212
+ consistency options now fail early instead of relying on SDK behavior.
213
+
214
+ Run `npm run test:unit` for deterministic mocked SQL/binding, decoding, shape,
215
+ cardinality, and option-forwarding tests. Run `npm run test:types` for a build
216
+ and strict public-declaration inference fixtures. These tests do not execute a
217
+ Couchbase server, prove an index plan, or measure freshness under concurrency.
218
+
219
+
220
+ ### Local server validation
221
+
222
+ On 2026-09-05, 20 live probes passed against the configured local Couchbase
223
+ Query 8.0.3 server. The probes used a fresh isolated scope, synthetic documents,
224
+ and dedicated primary/lookup indexes; that scope and all its contents were
225
+ removed afterward. They exercised all ANSI and ON KEYS join kinds, duplicate
226
+ JOIN rows, compound predicates and binding, missing LEFT JOIN relations, empty
227
+ LEFT NEST arrays, root/related date codecs and projections, raw ON, pagination,
228
+ alias rejection, immediate `request_plus` reads, and normal/joined reads using
229
+ real SDK mutation tokens via `consistentWith`.
230
+
231
+ The first run exposed an example error: `MutationState` takes `mutation.token`,
232
+ not the whole mutation result. The example is corrected and covered by a strict
233
+ type fixture; the corrected live run passed all 20 probes. These observations
234
+ validate this local server and synthetic workload; they do not establish a
235
+ concurrent multi-document snapshot, a production index plan, or compatibility
236
+ with every supported server version.
237
+
238
+
239
+ ### Array-path compatibility regression
240
+
241
+ The include identifier-quoting change initially quoted `media[*]` and
242
+ `participantIds[0]` as whole field names. The validated path parser restores
243
+ array traversal without interpolating SQL expressions or changing value binding:
244
+
245
+ ```ts
246
+ const results = await battles.findMany({
247
+ where: {'participantIds[0]': 'profile::demo'},
248
+ include: [{
249
+ as: 'mediaDocuments',
250
+ model: mediaModel,
251
+ keys: 'media[*].mediaId',
252
+ type: 'leftNest',
253
+ }],
254
+ });
255
+ // ON KEYS `doc`.`media`[*].`mediaId`
256
+ // Root _type filter plus `doc`.`participantIds`[0]=$cs_param_1
257
+ ```
258
+
259
+ For an opt-in regression probe, run `npm run build` then
260
+ `node scripts/live-array-path-probe.cjs`. It reads `.env`, requires a loopback
261
+ server, creates a uniquely named scope and indexes, and removes that scope in
262
+ `finally`. It writes a sanitized JSON report to the OS temporary directory.
263
+ Eight probes passed locally on Query 8.0.3 on 2026-09-05, verifying media
264
+ hydration/codecs, `[0]` and `[1]` filters, nested wildcard paths, aliases, sort
265
+ expressions, ANSI references, literal bracket-containing fields, and LEFT NEST
266
+ pagination. Nested result assertions compare membership, not source-key order.
@@ -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.2",
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",