vintasend-managed-templates 1.0.0-alpha2
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 +529 -0
- package/dist/base-template-manager-backend.d.ts +202 -0
- package/dist/base-template-manager-backend.d.ts.map +1 -0
- package/dist/base-template-manager-backend.js +1 -0
- package/dist/composition.d.ts +238 -0
- package/dist/composition.d.ts.map +1 -0
- package/dist/composition.js +0 -0
- package/dist/constants.d.ts +32 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +31 -0
- package/dist/errors.d.ts +80 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +86 -0
- package/dist/filter-evaluation.d.ts +69 -0
- package/dist/filter-evaluation.d.ts.map +1 -0
- package/dist/filter-evaluation.js +252 -0
- package/dist/filters.d.ts +192 -0
- package/dist/filters.d.ts.map +1 -0
- package/dist/filters.js +252 -0
- package/dist/in-memory-template-manager-backend.d.ts +85 -0
- package/dist/in-memory-template-manager-backend.d.ts.map +1 -0
- package/dist/in-memory-template-manager-backend.js +323 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/managed-template-renderer.d.ts +153 -0
- package/dist/managed-template-renderer.d.ts.map +1 -0
- package/dist/managed-template-renderer.js +152 -0
- package/dist/managed-template-service.d.ts +415 -0
- package/dist/managed-template-service.d.ts.map +1 -0
- package/dist/managed-template-service.js +712 -0
- package/dist/tags.d.ts +54 -0
- package/dist/tags.d.ts.map +1 -0
- package/dist/tags.js +108 -0
- package/dist/types.d.ts +100 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +41 -0
package/dist/errors.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every failure this package reports, as a class a caller can branch on.
|
|
3
|
+
*
|
|
4
|
+
* `instanceof` works across the whole hierarchy because each constructor restores the prototype
|
|
5
|
+
* chain — TypeScript compiled to an ES5-era target breaks it otherwise, and this package is
|
|
6
|
+
* consumed by applications that pick their own target.
|
|
7
|
+
*/
|
|
8
|
+
export class ManagedTemplateError extends Error {
|
|
9
|
+
constructor(message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = new.target.name;
|
|
12
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Raised when a template is not found in the backend. */
|
|
16
|
+
export class ManagedTemplateNotFoundError extends ManagedTemplateError {
|
|
17
|
+
}
|
|
18
|
+
/** Raised when a filter is malformed or names a field the vocabulary does not have. */
|
|
19
|
+
export class ManagedTemplateInvalidFilterError extends ManagedTemplateError {
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Raised when a listing asks for an order the configured backend cannot apply.
|
|
23
|
+
*
|
|
24
|
+
* This throws rather than quietly dropping the order, which is the difference between ordering
|
|
25
|
+
* and filtering: a dropped filter returns more rows than asked for, which a caller can see, while
|
|
26
|
+
* a dropped order returns the same rows in an arbitrary sequence that looks sorted. Read
|
|
27
|
+
* `getBackendSupportedFilterCapabilities()` and offer only the fields it reports.
|
|
28
|
+
*/
|
|
29
|
+
export class ManagedTemplateUnsupportedOrderingError extends ManagedTemplateError {
|
|
30
|
+
}
|
|
31
|
+
/** Raised when a status change is attributed to a user the backend cannot resolve. */
|
|
32
|
+
export class ManagedTemplateChangeUserNotFoundError extends ManagedTemplateError {
|
|
33
|
+
}
|
|
34
|
+
/** Raised when a status change is not allowed from the version's current status. */
|
|
35
|
+
export class ManagedTemplateStatusTransitionError extends ManagedTemplateError {
|
|
36
|
+
}
|
|
37
|
+
/** Raised when a tag is not found in the backend. */
|
|
38
|
+
export class ManagedTemplateTagNotFoundError extends ManagedTemplateError {
|
|
39
|
+
}
|
|
40
|
+
/** Raised when creating a tag whose text already slugs onto an existing tag. */
|
|
41
|
+
export class ManagedTemplateTagAlreadyExistsError extends ManagedTemplateError {
|
|
42
|
+
}
|
|
43
|
+
/** Raised when a tag's text is empty or has nothing that can be slugified. */
|
|
44
|
+
export class ManagedTemplateInvalidTagError extends ManagedTemplateError {
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Base class for every failure of {@link TemplateComposer}.
|
|
48
|
+
*
|
|
49
|
+
* Composition runs before the template engine does, so nothing here can be caught (or reported)
|
|
50
|
+
* by the engine downstream. Catch this to treat "the template could not be assembled" as one
|
|
51
|
+
* condition, or a subclass to tell a typo apart from a missing base.
|
|
52
|
+
*/
|
|
53
|
+
export class ManagedTemplateCompositionError extends ManagedTemplateError {
|
|
54
|
+
}
|
|
55
|
+
/** Raised when a `managed_*` composition tag is malformed, unknown or unbalanced. */
|
|
56
|
+
export class ManagedTemplateCompositionSyntaxError extends ManagedTemplateCompositionError {
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Raised when a template extends or includes a template that does not exist.
|
|
60
|
+
*
|
|
61
|
+
* The Python sibling makes this a `ManagedTemplateNotFoundError` as well, so code already
|
|
62
|
+
* handling a missing template keeps working. TypeScript has no multiple inheritance, so the
|
|
63
|
+
* relationship is carried by {@link isNotFoundError} instead — check with that rather than with
|
|
64
|
+
* a bare `instanceof ManagedTemplateNotFoundError`, and test for
|
|
65
|
+
* `ManagedTemplateCompositionError` *before* "not found" wherever the distinction matters: a
|
|
66
|
+
* base that does not exist is a broken composition of a template that does.
|
|
67
|
+
*/
|
|
68
|
+
export class ManagedTemplateCompositionReferenceError extends ManagedTemplateCompositionError {
|
|
69
|
+
}
|
|
70
|
+
/** Raised when a chain of extends/include references comes back to where it started. */
|
|
71
|
+
export class ManagedTemplateCompositionCycleError extends ManagedTemplateCompositionError {
|
|
72
|
+
}
|
|
73
|
+
/** Raised when a chain of references runs deeper than the composer's `maxDepth`. */
|
|
74
|
+
export class ManagedTemplateCompositionDepthError extends ManagedTemplateCompositionError {
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Whether an error means "the thing you asked for is not in the store".
|
|
78
|
+
*
|
|
79
|
+
* True for {@link ManagedTemplateNotFoundError} and for
|
|
80
|
+
* {@link ManagedTemplateCompositionReferenceError}, which is a missing template reached through
|
|
81
|
+
* a template that exists.
|
|
82
|
+
*/
|
|
83
|
+
export function isNotFoundError(error) {
|
|
84
|
+
return (error instanceof ManagedTemplateNotFoundError ||
|
|
85
|
+
error instanceof ManagedTemplateCompositionReferenceError);
|
|
86
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evaluating a `ManagedTemplateFilter` against templates already in hand.
|
|
3
|
+
*
|
|
4
|
+
* Not every store can push every filter down. A SQL backend can; a FHIR one cannot express `or`
|
|
5
|
+
* at all, and a document store may have no way to ask "does this row carry all of these tags".
|
|
6
|
+
* Rather than have each of those backends re-derive what a filter means — and disagree with the
|
|
7
|
+
* others on the corners — this module is the one implementation of the semantics, and a backend
|
|
8
|
+
* narrows server-side as far as it can and finishes here.
|
|
9
|
+
*
|
|
10
|
+
* That makes the fallback sound by construction: whatever a backend's query language can express
|
|
11
|
+
* is an optimization, and what it cannot express is still answered correctly.
|
|
12
|
+
*/
|
|
13
|
+
import { type ManagedTemplateStatus } from './constants.js';
|
|
14
|
+
import { type DateRange, type IntegerFieldFilter, type ManagedTemplateFilter, type ManagedTemplateOrderBy, type ManagedTemplateStatusFilter, type StringFieldFilter } from './filters.js';
|
|
15
|
+
import type { ManagedTemplate } from './types.js';
|
|
16
|
+
export type FilterEvaluationContext = {
|
|
17
|
+
/**
|
|
18
|
+
* Every version of one key, needed only by `mostRecentActiveVersion` — the one field whose
|
|
19
|
+
* answer depends on the *other* rows in the store rather than on the row being tested.
|
|
20
|
+
*
|
|
21
|
+
* A filter that names that field without this available cannot be answered, so
|
|
22
|
+
* {@link matchesTemplateFilter} throws rather than guessing.
|
|
23
|
+
*/
|
|
24
|
+
versionsOfKey?: (key: string) => readonly ManagedTemplate[];
|
|
25
|
+
};
|
|
26
|
+
export declare function matchesString(value: string, filter: StringFieldFilter): boolean;
|
|
27
|
+
export declare function matchesInteger(value: number, filter: IntegerFieldFilter): boolean;
|
|
28
|
+
export declare function matchesStatus(value: ManagedTemplateStatus, filter: ManagedTemplateStatusFilter): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Whether a date falls in a range.
|
|
31
|
+
*
|
|
32
|
+
* A row whose date is unset never matches a positive range — the same NULL rule `vintasend`'s
|
|
33
|
+
* notification date ranges follow — but `createdAt` and `updatedAt` are always set on a stored
|
|
34
|
+
* template, so this only comes up for a template assembled by hand.
|
|
35
|
+
*/
|
|
36
|
+
export declare function matchesDateRange(value: Date | null | undefined, range: DateRange): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Whether a row is its key's current version.
|
|
39
|
+
*
|
|
40
|
+
* "This row is active or draft, and no active-or-draft row of the same key is numbered higher."
|
|
41
|
+
* Written as a comparison against the key's other versions rather than as a lookup, because that
|
|
42
|
+
* is what the field means — see `ManagedTemplateFilterFields.mostRecentActiveVersion`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function isMostRecentActiveVersion(template: ManagedTemplate, versions: readonly ManagedTemplate[]): boolean;
|
|
45
|
+
/** Whether one template satisfies a filter, groups and all. */
|
|
46
|
+
export declare function matchesTemplateFilter(template: ManagedTemplate, filter: ManagedTemplateFilter, context?: FilterEvaluationContext): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Slugify a filter's tag values, dropping the ones that name no tag.
|
|
49
|
+
*
|
|
50
|
+
* A caller may name a tag by its slug or by the text behind it, so this is what makes
|
|
51
|
+
* `includesAllTags: ['Black Friday']` and `includesAllTags: ['black-friday']` the same query.
|
|
52
|
+
*/
|
|
53
|
+
export declare function normalizeSlugs(tags: readonly string[]): string[];
|
|
54
|
+
/** Slice a 1-indexed page out of a complete, ordered result set. */
|
|
55
|
+
/**
|
|
56
|
+
* Order templates by one field, for a backend whose store cannot do it.
|
|
57
|
+
*
|
|
58
|
+
* Sorting in memory is only correct over a **complete** result set — sort a page and you order
|
|
59
|
+
* rows within it while the rows chosen for it came back in the store's own order. So this is for
|
|
60
|
+
* a backend that reads everything anyway, and such a backend must still report `orderBy.*`
|
|
61
|
+
* honestly: it can sort, so it says `true`.
|
|
62
|
+
*
|
|
63
|
+
* The sort is total and stable. Every field falls back to `(key, version)` on a tie, so two rows
|
|
64
|
+
* that compare equal on the named field still come back in the same order on every call — a page
|
|
65
|
+
* boundary that moves between two requests silently drops or repeats rows.
|
|
66
|
+
*/
|
|
67
|
+
export declare function sortTemplates(templates: readonly ManagedTemplate[], orderBy: ManagedTemplateOrderBy | undefined): ManagedTemplate[];
|
|
68
|
+
export declare function paginate<Row>(rows: readonly Row[], page: number, pageSize: number): Row[];
|
|
69
|
+
//# sourceMappingURL=filter-evaluation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter-evaluation.d.ts","sourceRoot":"","sources":["../src/filter-evaluation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,KAAK,qBAAqB,EAAuC,MAAM,gBAAgB,CAAC;AACjG,OAAO,EACL,KAAK,SAAS,EACd,KAAK,kBAAkB,EAMvB,KAAK,qBAAqB,EAE1B,KAAK,sBAAsB,EAC3B,KAAK,2BAA2B,EAChC,KAAK,iBAAiB,EACvB,MAAM,cAAc,CAAC;AAEtB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,MAAM,MAAM,uBAAuB,GAAG;IACpC;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,SAAS,eAAe,EAAE,CAAC;CAC7D,CAAC;AAEF,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAmB/E;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAcjF;AAED,wBAAgB,aAAa,CAC3B,KAAK,EAAE,qBAAqB,EAC5B,MAAM,EAAE,2BAA2B,GAClC,OAAO,CAQT;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,IAAI,GAAG,IAAI,GAAG,SAAS,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAW1F;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,eAAe,EACzB,QAAQ,EAAE,SAAS,eAAe,EAAE,GACnC,OAAO,CAUT;AAED,+DAA+D;AAC/D,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,eAAe,EACzB,MAAM,EAAE,qBAAqB,EAC7B,OAAO,GAAE,uBAA4B,GACpC,OAAO,CAWT;AAyED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAEhE;AAED,oEAAoE;AACpE;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAC3B,SAAS,EAAE,SAAS,eAAe,EAAE,EACrC,OAAO,EAAE,sBAAsB,GAAG,SAAS,GAC1C,eAAe,EAAE,CAwBnB;AAuDD,wBAAgB,QAAQ,CAAC,GAAG,EAAE,IAAI,EAAE,SAAS,GAAG,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,GAAG,EAAE,CAGzF"}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evaluating a `ManagedTemplateFilter` against templates already in hand.
|
|
3
|
+
*
|
|
4
|
+
* Not every store can push every filter down. A SQL backend can; a FHIR one cannot express `or`
|
|
5
|
+
* at all, and a document store may have no way to ask "does this row carry all of these tags".
|
|
6
|
+
* Rather than have each of those backends re-derive what a filter means — and disagree with the
|
|
7
|
+
* others on the corners — this module is the one implementation of the semantics, and a backend
|
|
8
|
+
* narrows server-side as far as it can and finishes here.
|
|
9
|
+
*
|
|
10
|
+
* That makes the fallback sound by construction: whatever a backend's query language can express
|
|
11
|
+
* is an optimization, and what it cannot express is still answered correctly.
|
|
12
|
+
*/
|
|
13
|
+
import { MOST_RECENT_ACTIVE_VERSION_STATUSES } from './constants.js';
|
|
14
|
+
import { isFieldFilter, isNumericFilterLookup, isStatusExactLookup, isStatusInLookup, isStringFilterLookup, } from './filters.js';
|
|
15
|
+
import { slugifyTag } from './tags.js';
|
|
16
|
+
export function matchesString(value, filter) {
|
|
17
|
+
if (!isStringFilterLookup(filter)) {
|
|
18
|
+
return value === filter;
|
|
19
|
+
}
|
|
20
|
+
const caseSensitive = filter.caseSensitive ?? true;
|
|
21
|
+
const haystack = caseSensitive ? value : value.toLowerCase();
|
|
22
|
+
const needle = caseSensitive ? filter.value : filter.value.toLowerCase();
|
|
23
|
+
switch (filter.lookup) {
|
|
24
|
+
case 'exact':
|
|
25
|
+
return haystack === needle;
|
|
26
|
+
case 'startsWith':
|
|
27
|
+
return haystack.startsWith(needle);
|
|
28
|
+
case 'endsWith':
|
|
29
|
+
return haystack.endsWith(needle);
|
|
30
|
+
case 'includes':
|
|
31
|
+
return haystack.includes(needle);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function matchesInteger(value, filter) {
|
|
35
|
+
if (!isNumericFilterLookup(filter)) {
|
|
36
|
+
return value === filter;
|
|
37
|
+
}
|
|
38
|
+
switch (filter.lookup) {
|
|
39
|
+
case 'gt':
|
|
40
|
+
return value > filter.value;
|
|
41
|
+
case 'gte':
|
|
42
|
+
return value >= filter.value;
|
|
43
|
+
case 'lt':
|
|
44
|
+
return value < filter.value;
|
|
45
|
+
case 'lte':
|
|
46
|
+
return value <= filter.value;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function matchesStatus(value, filter) {
|
|
50
|
+
if (isStatusInLookup(filter)) {
|
|
51
|
+
return filter.value.includes(value);
|
|
52
|
+
}
|
|
53
|
+
if (isStatusExactLookup(filter)) {
|
|
54
|
+
return value === filter.value;
|
|
55
|
+
}
|
|
56
|
+
return value === filter;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Whether a date falls in a range.
|
|
60
|
+
*
|
|
61
|
+
* A row whose date is unset never matches a positive range — the same NULL rule `vintasend`'s
|
|
62
|
+
* notification date ranges follow — but `createdAt` and `updatedAt` are always set on a stored
|
|
63
|
+
* template, so this only comes up for a template assembled by hand.
|
|
64
|
+
*/
|
|
65
|
+
export function matchesDateRange(value, range) {
|
|
66
|
+
if (!(value instanceof Date)) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
if (range.from !== undefined && value.getTime() < range.from.getTime()) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
if (range.to !== undefined && value.getTime() > range.to.getTime()) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Whether a row is its key's current version.
|
|
79
|
+
*
|
|
80
|
+
* "This row is active or draft, and no active-or-draft row of the same key is numbered higher."
|
|
81
|
+
* Written as a comparison against the key's other versions rather than as a lookup, because that
|
|
82
|
+
* is what the field means — see `ManagedTemplateFilterFields.mostRecentActiveVersion`.
|
|
83
|
+
*/
|
|
84
|
+
export function isMostRecentActiveVersion(template, versions) {
|
|
85
|
+
if (!MOST_RECENT_ACTIVE_VERSION_STATUSES.includes(template.status)) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
return !versions.some((candidate) => candidate.key === template.key &&
|
|
89
|
+
MOST_RECENT_ACTIVE_VERSION_STATUSES.includes(candidate.status) &&
|
|
90
|
+
candidate.version > template.version);
|
|
91
|
+
}
|
|
92
|
+
/** Whether one template satisfies a filter, groups and all. */
|
|
93
|
+
export function matchesTemplateFilter(template, filter, context = {}) {
|
|
94
|
+
if (!isFieldFilter(filter)) {
|
|
95
|
+
if ('and' in filter) {
|
|
96
|
+
return filter.and.every((sub) => matchesTemplateFilter(template, sub, context));
|
|
97
|
+
}
|
|
98
|
+
if ('or' in filter) {
|
|
99
|
+
return filter.or.some((sub) => matchesTemplateFilter(template, sub, context));
|
|
100
|
+
}
|
|
101
|
+
return !matchesTemplateFilter(template, filter.not, context);
|
|
102
|
+
}
|
|
103
|
+
return matchesFields(template, filter, context);
|
|
104
|
+
}
|
|
105
|
+
function matchesFields(template, filters, context) {
|
|
106
|
+
if (filters.name !== undefined && !matchesString(template.name, filters.name)) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
if (filters.description !== undefined &&
|
|
110
|
+
!matchesString(template.description, filters.description)) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
if (filters.key !== undefined && !matchesString(template.key, filters.key)) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
if (filters.templateManagedBackend !== undefined &&
|
|
117
|
+
!matchesString(template.templateManagedBackend, filters.templateManagedBackend)) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
if (filters.version !== undefined && !matchesInteger(template.version, filters.version)) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
if (filters.status !== undefined && !matchesStatus(template.status, filters.status)) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
if (filters.createdAtRange !== undefined &&
|
|
127
|
+
!matchesDateRange(template.createdAt, filters.createdAtRange)) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
if (filters.updatedAtRange !== undefined &&
|
|
131
|
+
!matchesDateRange(template.updatedAt, filters.updatedAtRange)) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
if (filters.includesAllTags !== undefined) {
|
|
135
|
+
const carried = new Set(template.tags.map((tag) => tag.slug));
|
|
136
|
+
if (!normalizeSlugs(filters.includesAllTags).every((slug) => carried.has(slug))) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (filters.includesAnyOfTags !== undefined) {
|
|
141
|
+
const carried = new Set(template.tags.map((tag) => tag.slug));
|
|
142
|
+
if (!normalizeSlugs(filters.includesAnyOfTags).some((slug) => carried.has(slug))) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (filters.isAbstract !== undefined && template.isAbstract !== filters.isAbstract) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
if (filters.mostRecentActiveVersion !== undefined) {
|
|
150
|
+
if (context.versionsOfKey === undefined) {
|
|
151
|
+
throw new Error("mostRecentActiveVersion is answered against a key's other versions, so " +
|
|
152
|
+
'matchesTemplateFilter needs a versionsOfKey lookup to evaluate it.');
|
|
153
|
+
}
|
|
154
|
+
const current = isMostRecentActiveVersion(template, context.versionsOfKey(template.key));
|
|
155
|
+
if (current !== filters.mostRecentActiveVersion) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Slugify a filter's tag values, dropping the ones that name no tag.
|
|
163
|
+
*
|
|
164
|
+
* A caller may name a tag by its slug or by the text behind it, so this is what makes
|
|
165
|
+
* `includesAllTags: ['Black Friday']` and `includesAllTags: ['black-friday']` the same query.
|
|
166
|
+
*/
|
|
167
|
+
export function normalizeSlugs(tags) {
|
|
168
|
+
return tags.map((tag) => slugifyTag(tag)).filter(Boolean);
|
|
169
|
+
}
|
|
170
|
+
/** Slice a 1-indexed page out of a complete, ordered result set. */
|
|
171
|
+
/**
|
|
172
|
+
* Order templates by one field, for a backend whose store cannot do it.
|
|
173
|
+
*
|
|
174
|
+
* Sorting in memory is only correct over a **complete** result set — sort a page and you order
|
|
175
|
+
* rows within it while the rows chosen for it came back in the store's own order. So this is for
|
|
176
|
+
* a backend that reads everything anyway, and such a backend must still report `orderBy.*`
|
|
177
|
+
* honestly: it can sort, so it says `true`.
|
|
178
|
+
*
|
|
179
|
+
* The sort is total and stable. Every field falls back to `(key, version)` on a tie, so two rows
|
|
180
|
+
* that compare equal on the named field still come back in the same order on every call — a page
|
|
181
|
+
* boundary that moves between two requests silently drops or repeats rows.
|
|
182
|
+
*/
|
|
183
|
+
export function sortTemplates(templates, orderBy) {
|
|
184
|
+
const rows = [...templates];
|
|
185
|
+
if (orderBy === undefined) {
|
|
186
|
+
return rows;
|
|
187
|
+
}
|
|
188
|
+
const sign = orderBy.direction === 'desc' ? -1 : 1;
|
|
189
|
+
return rows.sort((left, right) => {
|
|
190
|
+
// Absent values sort last in *both* directions, so this is compared before the sign is
|
|
191
|
+
// applied. `createdAt` and `updatedAt` are typed non-null, but a backend hydrating them from
|
|
192
|
+
// a store that allows null would otherwise put its nulls first on `desc` and last on `asc`.
|
|
193
|
+
const nullness = compareNullness(left, right, orderBy.field);
|
|
194
|
+
if (nullness !== 0) {
|
|
195
|
+
return nullness;
|
|
196
|
+
}
|
|
197
|
+
const primary = compareField(left, right, orderBy.field);
|
|
198
|
+
if (primary !== 0) {
|
|
199
|
+
return primary * sign;
|
|
200
|
+
}
|
|
201
|
+
// The tiebreak is *not* reversed either: it exists to make the order total, and flipping it
|
|
202
|
+
// with the direction would make `asc` and `desc` disagree on which of two equal rows comes
|
|
203
|
+
// first — which is how a page boundary starts dropping and repeating rows.
|
|
204
|
+
return compareTiebreak(left, right);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
/** -1, 0 or 1 according to which side is missing a value for `field`; 0 when both have one. */
|
|
208
|
+
function compareNullness(left, right, field) {
|
|
209
|
+
const leftMissing = (left[field] ?? null) === null;
|
|
210
|
+
const rightMissing = (right[field] ?? null) === null;
|
|
211
|
+
if (leftMissing === rightMissing)
|
|
212
|
+
return 0;
|
|
213
|
+
return leftMissing ? 1 : -1;
|
|
214
|
+
}
|
|
215
|
+
function compareField(left, right, field) {
|
|
216
|
+
switch (field) {
|
|
217
|
+
case 'key':
|
|
218
|
+
return compareStrings(left.key, right.key);
|
|
219
|
+
case 'name':
|
|
220
|
+
return compareStrings(left.name, right.name);
|
|
221
|
+
case 'status':
|
|
222
|
+
return compareStrings(left.status, right.status);
|
|
223
|
+
case 'version':
|
|
224
|
+
return left.version - right.version;
|
|
225
|
+
case 'createdAt':
|
|
226
|
+
return compareDates(left.createdAt, right.createdAt);
|
|
227
|
+
case 'updatedAt':
|
|
228
|
+
return compareDates(left.updatedAt, right.updatedAt);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function compareTiebreak(left, right) {
|
|
232
|
+
return compareStrings(left.key, right.key) || left.version - right.version;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Compare by code point rather than by locale.
|
|
236
|
+
*
|
|
237
|
+
* `localeCompare` would order rows differently depending on where the process runs, which for a
|
|
238
|
+
* paginated read means two pages of the same listing served by two machines can disagree.
|
|
239
|
+
*/
|
|
240
|
+
function compareStrings(left, right) {
|
|
241
|
+
if (left === right)
|
|
242
|
+
return 0;
|
|
243
|
+
return left < right ? -1 : 1;
|
|
244
|
+
}
|
|
245
|
+
/** Only reached with two present values — `compareNullness` has already settled the rest. */
|
|
246
|
+
function compareDates(left, right) {
|
|
247
|
+
return (left?.getTime() ?? 0) - (right?.getTime() ?? 0);
|
|
248
|
+
}
|
|
249
|
+
export function paginate(rows, page, pageSize) {
|
|
250
|
+
const start = (page - 1) * pageSize;
|
|
251
|
+
return rows.slice(start, start + pageSize);
|
|
252
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The filter vocabulary a template-manager backend evaluates.
|
|
3
|
+
*
|
|
4
|
+
* Spelled exactly the way `vintasend` spells its notification filters — camelCase fields,
|
|
5
|
+
* camelCase string lookups (`startsWith`, not `starts_with`), a `DateRange` with `from`/`to`,
|
|
6
|
+
* and the same `and`/`or`/`not` grouping — so a caller that already builds one kind of filter
|
|
7
|
+
* needs no second set of rules for this one.
|
|
8
|
+
*/
|
|
9
|
+
import type { ManagedTemplateStatus } from './constants.js';
|
|
10
|
+
/** Date range filter with optional lower and upper bounds. */
|
|
11
|
+
export type DateRange = {
|
|
12
|
+
from?: Date;
|
|
13
|
+
to?: Date;
|
|
14
|
+
};
|
|
15
|
+
export type StringFilterLookup = {
|
|
16
|
+
lookup: 'exact' | 'startsWith' | 'endsWith' | 'includes';
|
|
17
|
+
value: string;
|
|
18
|
+
caseSensitive?: boolean;
|
|
19
|
+
};
|
|
20
|
+
/** A bare string is a case-sensitive `exact` match. */
|
|
21
|
+
export type StringFieldFilter = string | StringFilterLookup;
|
|
22
|
+
export type NumericFilterLookup = {
|
|
23
|
+
lookup: 'gt' | 'gte' | 'lt' | 'lte';
|
|
24
|
+
value: number;
|
|
25
|
+
};
|
|
26
|
+
export type IntegerFieldFilter = number | NumericFilterLookup;
|
|
27
|
+
export type ManagedTemplateStatusFilter = ManagedTemplateStatus | {
|
|
28
|
+
lookup: 'exact';
|
|
29
|
+
value: ManagedTemplateStatus;
|
|
30
|
+
} | {
|
|
31
|
+
lookup: 'in';
|
|
32
|
+
value: ManagedTemplateStatus[];
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* A tag filter is a plain list of tag slugs — no lookup wrapper, because which of the two
|
|
36
|
+
* matches is meant is already said by the field name (`includesAllTags` / `includesAnyOfTags`)
|
|
37
|
+
* rather than by a `lookup` key.
|
|
38
|
+
*
|
|
39
|
+
* Values are matched against `ManagedTemplateTag.slug`. Backends slugify what they are given
|
|
40
|
+
* first, so a caller may pass either the slug or the text it came from and get the same result.
|
|
41
|
+
*/
|
|
42
|
+
export type TagsFieldFilter = string[];
|
|
43
|
+
/**
|
|
44
|
+
* Leaf-level filter conditions. Every field present is combined with implicit AND.
|
|
45
|
+
*/
|
|
46
|
+
export type ManagedTemplateFilterFields = {
|
|
47
|
+
name?: StringFieldFilter;
|
|
48
|
+
description?: StringFieldFilter;
|
|
49
|
+
key?: StringFieldFilter;
|
|
50
|
+
version?: IntegerFieldFilter;
|
|
51
|
+
templateManagedBackend?: StringFieldFilter;
|
|
52
|
+
status?: ManagedTemplateStatusFilter;
|
|
53
|
+
createdAtRange?: DateRange;
|
|
54
|
+
updatedAtRange?: DateRange;
|
|
55
|
+
/**
|
|
56
|
+
* Tag membership. `includesAllTags` matches a template carrying every listed tag,
|
|
57
|
+
* `includesAnyOfTags` one carrying at least one of them. Both follow `Array.every` /
|
|
58
|
+
* `Array.some` on an empty list: an empty `includesAllTags` constrains nothing, an empty
|
|
59
|
+
* `includesAnyOfTags` matches nothing.
|
|
60
|
+
*/
|
|
61
|
+
includesAllTags?: TagsFieldFilter;
|
|
62
|
+
includesAnyOfTags?: TagsFieldFilter;
|
|
63
|
+
/**
|
|
64
|
+
* Bases, or templates to send. `true` keeps only the templates that declare a
|
|
65
|
+
* `{% managed_children %}` hole or blocks without extending anything; `false` keeps only the
|
|
66
|
+
* ones that do not.
|
|
67
|
+
*
|
|
68
|
+
* Answered against the stored `ManagedTemplate.isAbstract` — a field a backend writes on
|
|
69
|
+
* every write — rather than by parsing sources at query time, which is the whole reason the
|
|
70
|
+
* flag is denormalized: a picker that has to exclude bases would otherwise read and parse
|
|
71
|
+
* every row in the store to draw one page.
|
|
72
|
+
*/
|
|
73
|
+
isAbstract?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* One row per key instead of one row per version. `true` keeps, for each key, only the
|
|
76
|
+
* highest-numbered version whose status is in `MOST_RECENT_ACTIVE_VERSION_STATUSES` (`active`
|
|
77
|
+
* or `draft`), and drops every key with no such version. `false` is its exact complement —
|
|
78
|
+
* every other row, retired keys included — so it is what `{ not: { mostRecentActiveVersion:
|
|
79
|
+
* true } }` means.
|
|
80
|
+
*
|
|
81
|
+
* This is the one field whose answer depends on the *other* rows in the store rather than on
|
|
82
|
+
* the row being tested, so a backend evaluates it against the whole key, not the row.
|
|
83
|
+
*/
|
|
84
|
+
mostRecentActiveVersion?: boolean;
|
|
85
|
+
};
|
|
86
|
+
export type ManagedTemplateFilter = ManagedTemplateFilterFields | {
|
|
87
|
+
and: ManagedTemplateFilter[];
|
|
88
|
+
} | {
|
|
89
|
+
or: ManagedTemplateFilter[];
|
|
90
|
+
} | {
|
|
91
|
+
not: ManagedTemplateFilter;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* The fields `ManagedTemplateFilterFields` accepts, as data.
|
|
95
|
+
*
|
|
96
|
+
* Kept in step with the type by {@link ManagedTemplateFilterFieldName}, which fails to compile
|
|
97
|
+
* if the two drift apart — a field added to the type without a name here is a type error rather
|
|
98
|
+
* than a filter the service silently rejects.
|
|
99
|
+
*/
|
|
100
|
+
export declare const KNOWN_FILTER_FIELDS: readonly ["name", "description", "key", "version", "templateManagedBackend", "status", "createdAtRange", "updatedAtRange", "includesAllTags", "includesAnyOfTags", "isAbstract", "mostRecentActiveVersion"];
|
|
101
|
+
/** The filter fields whose value is a list of tag slugs rather than a lookup object. */
|
|
102
|
+
export declare const TAG_FILTER_FIELDS: readonly ["includesAllTags", "includesAnyOfTags"];
|
|
103
|
+
/** The filter fields whose value is a bare boolean. */
|
|
104
|
+
export declare const FLAG_FILTER_FIELDS: readonly ["mostRecentActiveVersion", "isAbstract"];
|
|
105
|
+
/** True when `filter` is a field filter rather than an `and`/`or`/`not` group. */
|
|
106
|
+
export declare function isFieldFilter(filter: ManagedTemplateFilter): filter is ManagedTemplateFilterFields;
|
|
107
|
+
/** True when a string-field filter is a `StringFilterLookup` and not a bare string. */
|
|
108
|
+
export declare function isStringFilterLookup(value: unknown): value is StringFilterLookup;
|
|
109
|
+
export declare function isNumericFilterLookup(value: unknown): value is NumericFilterLookup;
|
|
110
|
+
export declare function isDateRange(value: unknown): value is DateRange;
|
|
111
|
+
export declare function isStatusInLookup(value: unknown): value is {
|
|
112
|
+
lookup: 'in';
|
|
113
|
+
value: ManagedTemplateStatus[];
|
|
114
|
+
};
|
|
115
|
+
export declare function isStatusExactLookup(value: unknown): value is {
|
|
116
|
+
lookup: 'exact';
|
|
117
|
+
value: ManagedTemplateStatus;
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* True when a value is a list of tag slugs.
|
|
121
|
+
*
|
|
122
|
+
* A bare string is deliberately rejected: `{ includesAllTags: 'welcome' }` would otherwise be
|
|
123
|
+
* spread character by character by a backend that iterates it, and silently ask for the tags
|
|
124
|
+
* `w`, `e`, `l`. Pass a one-element array instead.
|
|
125
|
+
*/
|
|
126
|
+
export declare function isTagsFilter(value: unknown): value is TagsFieldFilter;
|
|
127
|
+
/**
|
|
128
|
+
* Which filters a template-manager backend can honour.
|
|
129
|
+
*
|
|
130
|
+
* Follows the same rule as `vintasend`'s notification capabilities: **a backend declares only
|
|
131
|
+
* what it cannot do.** Its report is merged *over* an all-`true` default, so a filter field
|
|
132
|
+
* added in a later release does not force every backend to re-declare support for it, and a
|
|
133
|
+
* missing key means "supported".
|
|
134
|
+
*
|
|
135
|
+
* `orderBy.*` keys report which fields a backend can sort a page by. They are new vocabulary, so
|
|
136
|
+
* they default to **false**: a backend that predates ordering ignores the argument, and claiming
|
|
137
|
+
* an order it does not apply is worse than admitting it cannot — a caller that knows gets to
|
|
138
|
+
* decide, while one that is lied to silently renders rows in an arbitrary order and calls it
|
|
139
|
+
* sorted.
|
|
140
|
+
*
|
|
141
|
+
* Ordering has to reach the store rather than being applied to the page afterwards. Sorting a
|
|
142
|
+
* page after the fact orders rows *within* the page while the rows chosen *for* it stayed in the
|
|
143
|
+
* backend's own order, which looks right on page 1 and is wrong everywhere after it.
|
|
144
|
+
*/
|
|
145
|
+
export type ManagedTemplateFilterCapabilities = {
|
|
146
|
+
[key: string]: boolean;
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* The fields a template listing can be ordered by.
|
|
150
|
+
*
|
|
151
|
+
* Deliberately narrow: each one is a scalar the backend already stores per row, so a store can
|
|
152
|
+
* answer it with an index rather than a computed sort. `tags` is absent because ordering by a
|
|
153
|
+
* many-to-many has no single value to compare, and `mostRecentActiveVersion` because it is a
|
|
154
|
+
* filter rather than a field.
|
|
155
|
+
*/
|
|
156
|
+
export type ManagedTemplateOrderByField = 'key' | 'name' | 'version' | 'status' | 'createdAt' | 'updatedAt';
|
|
157
|
+
export type ManagedTemplateOrderDirection = 'asc' | 'desc';
|
|
158
|
+
export type ManagedTemplateOrderBy = {
|
|
159
|
+
field: ManagedTemplateOrderByField;
|
|
160
|
+
direction: ManagedTemplateOrderDirection;
|
|
161
|
+
};
|
|
162
|
+
/** Every orderable field, for validation and for building a capability report. */
|
|
163
|
+
export declare const MANAGED_TEMPLATE_ORDER_BY_FIELDS: readonly ManagedTemplateOrderByField[];
|
|
164
|
+
/** The capability key reporting whether a backend can order by `field`. */
|
|
165
|
+
export declare function orderByCapabilityKey(field: ManagedTemplateOrderByField): string;
|
|
166
|
+
export declare const DEFAULT_TEMPLATE_BACKEND_FILTER_CAPABILITIES: ManagedTemplateFilterCapabilities;
|
|
167
|
+
/**
|
|
168
|
+
* Drop the parts of a filter the backend has declared it cannot answer.
|
|
169
|
+
*
|
|
170
|
+
* This is the other half of the capability report: reporting a limitation is only useful if
|
|
171
|
+
* something acts on it. Every caller that builds a filter would otherwise have to walk the
|
|
172
|
+
* capability map itself and reach its own conclusions, and they would disagree.
|
|
173
|
+
*
|
|
174
|
+
* **Dropping widens.** A pruned filter matches everything the original did and possibly more, so
|
|
175
|
+
* a caller sees extra rows rather than missing ones — a listing that could not collapse to one
|
|
176
|
+
* row per key shows every version, which is visible in the result. That is the whole reason
|
|
177
|
+
* filters are negotiated by dropping while *ordering* is negotiated by refusing: an ignored order
|
|
178
|
+
* leaves no trace in the rows at all. Check the report before trusting a filter to have narrowed.
|
|
179
|
+
*
|
|
180
|
+
* Returns an empty filter — which constrains nothing — when everything has been dropped.
|
|
181
|
+
*/
|
|
182
|
+
export declare function pruneUnsupportedFilters(filter: ManagedTemplateFilter, capabilities: ManagedTemplateFilterCapabilities): ManagedTemplateFilter;
|
|
183
|
+
/** True for a filter that constrains nothing, whatever shape it arrived in. */
|
|
184
|
+
export declare function isEmptyFilter(filter: ManagedTemplateFilter): boolean;
|
|
185
|
+
/**
|
|
186
|
+
* Read one capability, defaulting to supported.
|
|
187
|
+
*
|
|
188
|
+
* A missing key means "supported": backends declare only what they *cannot* do, so a capability
|
|
189
|
+
* added in a later release does not force every backend to re-declare it.
|
|
190
|
+
*/
|
|
191
|
+
export declare function supportsCapability(capabilities: ManagedTemplateFilterCapabilities, key: string): boolean;
|
|
192
|
+
//# sourceMappingURL=filters.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filters.d.ts","sourceRoot":"","sources":["../src/filters.ts"],"names":[],"mappings":"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"}
|