@entifix/core 0.1.4 → 0.3.0
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/dist/entity-definition/describe/describe-child-columns.d.ts +3 -0
- package/dist/entity-definition/describe/describe-child-columns.d.ts.map +1 -1
- package/dist/entity-definition/describe/describe-child-columns.js +11 -0
- package/dist/entity-definition/describe/describe-child-columns.js.map +1 -1
- package/dist/entity-definition/describe/describe-entity-columns.d.ts +7 -1
- package/dist/entity-definition/describe/describe-entity-columns.d.ts.map +1 -1
- package/dist/entity-definition/describe/describe-entity-columns.js +41 -4
- package/dist/entity-definition/describe/describe-entity-columns.js.map +1 -1
- package/dist/entity-definition/describe/entity-view.d.ts +19 -0
- package/dist/entity-definition/describe/entity-view.d.ts.map +1 -0
- package/dist/entity-definition/describe/entity-view.js +35 -0
- package/dist/entity-definition/describe/entity-view.js.map +1 -0
- package/dist/entity-definition/describe/index.d.ts +2 -0
- package/dist/entity-definition/describe/index.d.ts.map +1 -1
- package/dist/entity-definition/describe/index.js +2 -0
- package/dist/entity-definition/describe/index.js.map +1 -1
- package/dist/entity-definition/describe/query-property.d.ts +17 -0
- package/dist/entity-definition/describe/query-property.d.ts.map +1 -0
- package/dist/entity-definition/describe/query-property.js +18 -0
- package/dist/entity-definition/describe/query-property.js.map +1 -0
- package/dist/entity-definition/meta-entities/bounds.d.ts +10 -0
- package/dist/entity-definition/meta-entities/bounds.d.ts.map +1 -0
- package/dist/entity-definition/meta-entities/bounds.js +17 -0
- package/dist/entity-definition/meta-entities/bounds.js.map +1 -0
- package/dist/entity-definition/meta-entities/meta-accessor/meta-accessor.d.ts +24 -1
- package/dist/entity-definition/meta-entities/meta-accessor/meta-accessor.d.ts.map +1 -1
- package/dist/entity-definition/meta-entities/meta-accessor/meta-accessor.js +8 -1
- package/dist/entity-definition/meta-entities/meta-accessor/meta-accessor.js.map +1 -1
- package/dist/entity-definition/meta-entities/meta-entity/meta-entity.d.ts +45 -0
- package/dist/entity-definition/meta-entities/meta-entity/meta-entity.d.ts.map +1 -1
- package/dist/entity-definition/meta-entities/meta-entity/meta-entity.js +4 -0
- package/dist/entity-definition/meta-entities/meta-entity/meta-entity.js.map +1 -1
- package/dist/entity-definition/reconstruct/coerce-field-value.d.ts +7 -0
- package/dist/entity-definition/reconstruct/coerce-field-value.d.ts.map +1 -1
- package/dist/entity-definition/reconstruct/coerce-field-value.js +9 -0
- package/dist/entity-definition/reconstruct/coerce-field-value.js.map +1 -1
- package/dist/entity-definition/reconstruct/reconstruct-entity.d.ts.map +1 -1
- package/dist/entity-definition/reconstruct/reconstruct-entity.js +8 -2
- package/dist/entity-definition/reconstruct/reconstruct-entity.js.map +1 -1
- package/dist/entity-definition/serializer/serialize.d.ts +13 -1
- package/dist/entity-definition/serializer/serialize.d.ts.map +1 -1
- package/dist/entity-definition/serializer/serialize.js +14 -2
- package/dist/entity-definition/serializer/serialize.js.map +1 -1
- package/dist/entity-definition/validate/index.d.ts +2 -0
- package/dist/entity-definition/validate/index.d.ts.map +1 -0
- package/dist/entity-definition/validate/index.js +3 -0
- package/dist/entity-definition/validate/index.js.map +1 -0
- package/dist/entity-definition/validate/validate-records.d.ts +75 -0
- package/dist/entity-definition/validate/validate-records.d.ts.map +1 -0
- package/dist/entity-definition/validate/validate-records.js +205 -0
- package/dist/entity-definition/validate/validate-records.js.map +1 -0
- package/dist/i18n/locales.d.ts +46 -7
- package/dist/i18n/locales.d.ts.map +1 -1
- package/dist/i18n/locales.js +49 -10
- package/dist/i18n/locales.js.map +1 -1
- package/dist/i18n/localized-text.d.ts +27 -0
- package/dist/i18n/localized-text.d.ts.map +1 -0
- package/dist/i18n/localized-text.js +18 -0
- package/dist/i18n/localized-text.js.map +1 -0
- package/dist/i18n/negotiate.d.ts +1 -1
- package/dist/i18n/negotiate.d.ts.map +1 -1
- package/dist/i18n/negotiate.js +3 -3
- package/dist/i18n/negotiate.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/query/in-memory-query.d.ts +49 -0
- package/dist/query/in-memory-query.d.ts.map +1 -0
- package/dist/query/in-memory-query.js +133 -0
- package/dist/query/in-memory-query.js.map +1 -0
- package/dist/rsql/coerce-rsql.d.ts +21 -0
- package/dist/rsql/coerce-rsql.d.ts.map +1 -1
- package/dist/rsql/coerce-rsql.js +44 -8
- package/dist/rsql/coerce-rsql.js.map +1 -1
- package/dist/rsql/sort-param.d.ts.map +1 -1
- package/dist/rsql/sort-param.js +3 -9
- package/dist/rsql/sort-param.js.map +1 -1
- package/dist/types/EntityDraft.d.ts +18 -2
- package/dist/types/EntityDraft.d.ts.map +1 -1
- package/dist/types/EntityDraft.js +11 -0
- package/dist/types/EntityDraft.js.map +1 -1
- package/package.json +3 -4
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
import { EntifixBuildError } from '../../base-entities/entifix-error/index.js';
|
|
3
|
+
import { locales } from '../../i18n/locales.js';
|
|
4
|
+
import { COLLECTION_TYPES, describeEntityColumns, extractEntityView } from '../describe/index.js';
|
|
5
|
+
import { extractMetaEntity } from '../helpers/index.js';
|
|
6
|
+
/** Thrown by {@link validRecordsOrThrow}. Carries every problem found. */ export class RecordValidationError extends EntifixBuildError {
|
|
7
|
+
constructor(problems){
|
|
8
|
+
super(`Records are invalid:\n${problems.map((problem)=>` ${problem.path} ${problem.message}`).join('\n')}`, undefined, {
|
|
9
|
+
count: problems.length
|
|
10
|
+
});
|
|
11
|
+
this.problems = problems;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const isPlainObject = (value)=>value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
15
|
+
const isAbsent = (value)=>value === undefined || value === null || value === '';
|
|
16
|
+
const counted = (n, noun)=>`${n} ${noun}${n === 1 ? '' : 's'}`;
|
|
17
|
+
/** A date member, parsed: the mapping would leave it a string. */ function parseDate(value, report, member) {
|
|
18
|
+
if (value instanceof Date) {
|
|
19
|
+
if (Number.isNaN(value.getTime())) report(member, 'is an invalid date');
|
|
20
|
+
return Number.isNaN(value.getTime()) ? undefined : value;
|
|
21
|
+
}
|
|
22
|
+
if (typeof value !== 'string' && typeof value !== 'number') {
|
|
23
|
+
report(member, `holds a ${typeof value}, not a date`);
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
const parsed = new Date(value);
|
|
27
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
28
|
+
report(member, `is not a date: ${JSON.stringify(value)}`);
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
return parsed;
|
|
32
|
+
}
|
|
33
|
+
/** A relation: ids, or embedded records, and ids that exist. */ function checkLink(value, descriptor, known, report) {
|
|
34
|
+
const isCollection = descriptor.type === 'linkCollection';
|
|
35
|
+
if (isCollection && !Array.isArray(value)) {
|
|
36
|
+
report(descriptor.name, 'is not a list');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
for (const id of isCollection ? value : [
|
|
40
|
+
value
|
|
41
|
+
]){
|
|
42
|
+
if (isPlainObject(id)) continue; // An embedded record carries its own.
|
|
43
|
+
if (typeof id !== 'string' && typeof id !== 'number') {
|
|
44
|
+
report(descriptor.name, `holds a ${typeof id} where an id was expected`);
|
|
45
|
+
} else if (known !== undefined && !known.has(id)) {
|
|
46
|
+
report(descriptor.name, `points at ${JSON.stringify(id)}, which does not exist`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** A scalar: the type the metadata declares. */ function checkScalar(value, descriptor, report) {
|
|
51
|
+
switch(descriptor.type){
|
|
52
|
+
case 'number':
|
|
53
|
+
if (typeof value !== 'number' || Number.isNaN(value)) {
|
|
54
|
+
report(descriptor.name, `holds ${JSON.stringify(value)}, not a number`);
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
case 'boolean':
|
|
58
|
+
if (typeof value !== 'boolean') {
|
|
59
|
+
report(descriptor.name, `holds ${JSON.stringify(value)}, not a boolean`);
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
case 'enum':
|
|
63
|
+
var _descriptor_enumValues;
|
|
64
|
+
if (!((_descriptor_enumValues = descriptor.enumValues) == null ? void 0 : _descriptor_enumValues.includes(value))) {
|
|
65
|
+
var _descriptor_enumValues1;
|
|
66
|
+
report(descriptor.name, `holds ${JSON.stringify(value)}, which is not one of ${JSON.stringify((_descriptor_enumValues1 = descriptor.enumValues) != null ? _descriptor_enumValues1 : [])}`);
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
case 'scalarCollection':
|
|
70
|
+
case 'composition':
|
|
71
|
+
if (!Array.isArray(value)) report(descriptor.name, 'is not a list');
|
|
72
|
+
return;
|
|
73
|
+
default:
|
|
74
|
+
// `string` and `id`, the two that carry text.
|
|
75
|
+
if (typeof value !== 'string' && typeof value !== 'number') {
|
|
76
|
+
report(descriptor.name, `holds a ${typeof value}, not text`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* A localized member: an object with non-empty text under every locale. A
|
|
82
|
+
* missing locale fails rather than falls back, so a page never shows one
|
|
83
|
+
* language in the middle of another.
|
|
84
|
+
*/ function checkLocalized(value, descriptor, locales, report) {
|
|
85
|
+
if (!isPlainObject(value)) {
|
|
86
|
+
report(descriptor.name, 'is not a text per locale');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
for (const locale of locales){
|
|
90
|
+
const text = value[locale];
|
|
91
|
+
if (text === undefined) {
|
|
92
|
+
report(descriptor.name, `is missing "${locale}"`);
|
|
93
|
+
} else if (typeof text !== 'string') {
|
|
94
|
+
report(descriptor.name, `holds a ${typeof text} for "${locale}", not text`);
|
|
95
|
+
} else if (text.trim() === '') {
|
|
96
|
+
report(descriptor.name, `is empty for "${locale}"`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** `minItems` / `maxItems` on a collection that is present and is a list. */ function checkItemCount(value, descriptor, report) {
|
|
101
|
+
if (!Array.isArray(value)) return;
|
|
102
|
+
const { minItems, maxItems } = descriptor;
|
|
103
|
+
if (minItems !== undefined && value.length < minItems) {
|
|
104
|
+
report(descriptor.name, `holds ${counted(value.length, 'item')}, and needs at least ${minItems}`);
|
|
105
|
+
}
|
|
106
|
+
if (maxItems !== undefined && value.length > maxItems) {
|
|
107
|
+
report(descriptor.name, `holds ${counted(value.length, 'item')}, and takes at most ${maxItems}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Checks every record of one entity against its metadata — identity, `required`,
|
|
112
|
+
* each member's type, link ids, item counts, `notBefore`, and the entity's
|
|
113
|
+
* cardinality — then runs the caller's rules.
|
|
114
|
+
*/ export function validateRecords(entityConstructor, records, { source, linkTargets = {}, rules = [], locales: required = locales(), view } = {}) {
|
|
115
|
+
const problems = [];
|
|
116
|
+
const descriptors = describeEntityColumns(entityConstructor);
|
|
117
|
+
const omitted = new Set(view === undefined ? [] : extractEntityView(entityConstructor, view).omitted.map((descriptor)=>descriptor.name));
|
|
118
|
+
const parsed = [];
|
|
119
|
+
const seen = new Set();
|
|
120
|
+
const at = (where, member, message)=>problems.push({
|
|
121
|
+
path: [
|
|
122
|
+
source,
|
|
123
|
+
where,
|
|
124
|
+
member
|
|
125
|
+
].filter(Boolean).join(' › '),
|
|
126
|
+
message
|
|
127
|
+
});
|
|
128
|
+
records.forEach((raw, index)=>{
|
|
129
|
+
if (!isPlainObject(raw)) {
|
|
130
|
+
at(`#${index}`, undefined, 'is not a record');
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const id = raw['id'];
|
|
134
|
+
const report = (member, message)=>at(isAbsent(id) ? `#${index}` : String(id), member, message);
|
|
135
|
+
if (isAbsent(id)) {
|
|
136
|
+
report('id', 'is missing');
|
|
137
|
+
} else if (seen.has(id)) {
|
|
138
|
+
report('id', 'is used more than once');
|
|
139
|
+
} else {
|
|
140
|
+
seen.add(id);
|
|
141
|
+
}
|
|
142
|
+
const record = _extends({}, raw);
|
|
143
|
+
for (const descriptor of descriptors){
|
|
144
|
+
if (descriptor.name === 'id') continue;
|
|
145
|
+
const value = raw[descriptor.key];
|
|
146
|
+
if (isAbsent(value)) {
|
|
147
|
+
if (descriptor.required && !omitted.has(descriptor.name)) {
|
|
148
|
+
report(descriptor.name, 'is required, and is missing');
|
|
149
|
+
}
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (descriptor.type === 'link' || descriptor.type === 'linkCollection') {
|
|
153
|
+
checkLink(value, descriptor, linkTargets[descriptor.name], report);
|
|
154
|
+
} else if (descriptor.type === 'localized') {
|
|
155
|
+
checkLocalized(value, descriptor, required, report);
|
|
156
|
+
} else if (descriptor.type === 'date') {
|
|
157
|
+
const date = parseDate(value, report, descriptor.name);
|
|
158
|
+
if (date !== undefined) record[descriptor.key] = date;
|
|
159
|
+
} else {
|
|
160
|
+
checkScalar(value, descriptor, report);
|
|
161
|
+
}
|
|
162
|
+
if (COLLECTION_TYPES.includes(descriptor.type)) {
|
|
163
|
+
checkItemCount(value, descriptor, report);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
for (const descriptor of descriptors){
|
|
167
|
+
if (descriptor.notBefore === undefined) continue;
|
|
168
|
+
const earlier = descriptors.find((other)=>other.name === descriptor.notBefore);
|
|
169
|
+
const end = record[descriptor.key];
|
|
170
|
+
const start = record[earlier.key];
|
|
171
|
+
if (start instanceof Date && end instanceof Date && end < start) {
|
|
172
|
+
report(descriptor.name, `is before ${earlier.name}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
parsed.push(record);
|
|
176
|
+
});
|
|
177
|
+
const { cardinality, name } = extractMetaEntity(entityConstructor);
|
|
178
|
+
const where = `${counted(records.length, 'record')}`;
|
|
179
|
+
if ((cardinality == null ? void 0 : cardinality.min) !== undefined && records.length < cardinality.min) {
|
|
180
|
+
at(name, undefined, `has ${where}, and needs at least ${cardinality.min}`);
|
|
181
|
+
}
|
|
182
|
+
if ((cardinality == null ? void 0 : cardinality.max) !== undefined && records.length > cardinality.max) {
|
|
183
|
+
at(name, undefined, `has ${where}, and takes at most ${cardinality.max}`);
|
|
184
|
+
}
|
|
185
|
+
for (const rule of rules){
|
|
186
|
+
rule(parsed, (index, member, message)=>{
|
|
187
|
+
var _parsed_index;
|
|
188
|
+
const id = (_parsed_index = parsed[index]) == null ? void 0 : _parsed_index['id'];
|
|
189
|
+
at(isAbsent(id) ? `#${index}` : String(id), member, message);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
problems,
|
|
194
|
+
records: parsed
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/** The records with their dates parsed, or a {@link RecordValidationError}. */ export function validRecordsOrThrow(entityConstructor, records, options) {
|
|
198
|
+
const result = validateRecords(entityConstructor, records, options);
|
|
199
|
+
if (result.problems.length > 0) {
|
|
200
|
+
throw new RecordValidationError(result.problems);
|
|
201
|
+
}
|
|
202
|
+
return result.records;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
//# sourceMappingURL=validate-records.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/entity-definition/validate/validate-records.ts"],"sourcesContent":["import { EntifixBuildError } from '../../base-entities/entifix-error/index.js';\nimport { locales } from '../../i18n/locales.js';\nimport type {\n Entity,\n EntityConstructor,\n EntityId,\n} from '../../types/Entity.js';\nimport {\n COLLECTION_TYPES,\n describeEntityColumns,\n type EntityFieldDescriptor,\n extractEntityView,\n} from '../describe/index.js';\nimport { extractMetaEntity } from '../helpers/index.js';\n\n/**\n * Records checked against their entity's metadata before anything serves them.\n *\n * The mapping does not do this, on purpose: `deserializeSingleEntity` assigns\n * every non-relation value raw — it converts no date, enforces no `required`,\n * validates no enum value and resolves no link id — because a record off a\n * database was written through that same mapping and needs none of it. Records\n * that arrive as **untyped JSON** — files bundled with a static build, an\n * import, a seed — have had no such guarantee, and a wrong one reaches a reader\n * as `undefined` in the middle of a page, or as a string where a `Date` was\n * expected. This is the check those records go through.\n *\n * It sits in core, beside the deserializer it makes up for, so every adapter\n * that reads untyped records shares it — and the generated form judges a\n * draft by the same metadata, so a record a form refuses is one this refuses.\n *\n * Every problem is collected rather than the first thrown, each with the path\n * to its value: `employment.json › acme › role is required, and is missing`.\n */\n\nexport interface RecordProblem {\n /** `<source> › <id> › <member>`, as far as each is known. */\n readonly path: string;\n readonly message: string;\n}\n\n/** Thrown by {@link validRecordsOrThrow}. Carries every problem found. */\nexport class RecordValidationError extends EntifixBuildError {\n readonly problems: readonly RecordProblem[];\n\n constructor(problems: readonly RecordProblem[]) {\n super(\n `Records are invalid:\\n${problems\n .map(problem => ` ${problem.path} ${problem.message}`)\n .join('\\n')}`,\n undefined,\n { count: problems.length },\n );\n this.problems = problems;\n }\n}\n\n/**\n * A check one entity needs that metadata cannot express — at most one contact\n * channel per type, say. Handed every record (dates already parsed) and a way\n * to report against one of them by index.\n */\nexport type EntityRecordRule = (\n records: ReadonlyArray<Readonly<Record<string, unknown>>>,\n report: (index: number, member: string | undefined, message: string) => void,\n) => void;\n\nexport interface ValidateRecordsOptions {\n /** Where the records came from, for the path in a message. */\n readonly source?: string;\n /**\n * The ids that exist for each `link` or `linkCollection` member, by member\n * name. A member left out is checked for shape only.\n */\n readonly linkTargets?: Readonly<Record<string, ReadonlySet<EntityId>>>;\n /** Checks of the caller's own, run after the metadata's. */\n readonly rules?: readonly EntityRecordRule[];\n /**\n * Every locale a `localized` member must carry. Defaults to the locales the\n * host declared through `defineLocales`.\n */\n readonly locales?: readonly string[];\n /**\n * The view the records were published in. A member it omits is not\n * required of them, since the view left it out on purpose.\n */\n readonly view?: string;\n}\n\nexport interface ValidateRecordsResult {\n readonly problems: readonly RecordProblem[];\n /**\n * The records with every `date` member parsed into a `Date`, ready for the\n * mapping. Meaningless when `problems` is not empty.\n */\n readonly records: ReadonlyArray<Record<string, unknown>>;\n}\n\nconst isPlainObject = (value: unknown): value is Record<string, unknown> =>\n value !== null && typeof value === 'object' && !Array.isArray(value);\n\nconst isAbsent = (value: unknown): boolean =>\n value === undefined || value === null || value === '';\n\ntype Report = (member: string | undefined, message: string) => void;\n\nconst counted = (n: number, noun: string) =>\n `${n} ${noun}${n === 1 ? '' : 's'}`;\n\n/** A date member, parsed: the mapping would leave it a string. */\nfunction parseDate(\n value: unknown,\n report: Report,\n member: string,\n): Date | undefined {\n if (value instanceof Date) {\n if (Number.isNaN(value.getTime())) report(member, 'is an invalid date');\n return Number.isNaN(value.getTime()) ? undefined : value;\n }\n if (typeof value !== 'string' && typeof value !== 'number') {\n report(member, `holds a ${typeof value}, not a date`);\n return undefined;\n }\n const parsed = new Date(value);\n if (Number.isNaN(parsed.getTime())) {\n report(member, `is not a date: ${JSON.stringify(value)}`);\n return undefined;\n }\n return parsed;\n}\n\n/** A relation: ids, or embedded records, and ids that exist. */\nfunction checkLink(\n value: unknown,\n descriptor: EntityFieldDescriptor,\n known: ReadonlySet<EntityId> | undefined,\n report: Report,\n): void {\n const isCollection = descriptor.type === 'linkCollection';\n if (isCollection && !Array.isArray(value)) {\n report(descriptor.name, 'is not a list');\n return;\n }\n for (const id of isCollection ? (value as unknown[]) : [value]) {\n if (isPlainObject(id)) continue; // An embedded record carries its own.\n if (typeof id !== 'string' && typeof id !== 'number') {\n report(descriptor.name, `holds a ${typeof id} where an id was expected`);\n } else if (known !== undefined && !known.has(id)) {\n report(\n descriptor.name,\n `points at ${JSON.stringify(id)}, which does not exist`,\n );\n }\n }\n}\n\n/** A scalar: the type the metadata declares. */\nfunction checkScalar(\n value: unknown,\n descriptor: EntityFieldDescriptor,\n report: Report,\n): void {\n switch (descriptor.type) {\n case 'number':\n if (typeof value !== 'number' || Number.isNaN(value)) {\n report(descriptor.name, `holds ${JSON.stringify(value)}, not a number`);\n }\n return;\n case 'boolean':\n if (typeof value !== 'boolean') {\n report(\n descriptor.name,\n `holds ${JSON.stringify(value)}, not a boolean`,\n );\n }\n return;\n case 'enum':\n if (!descriptor.enumValues?.includes(value as string)) {\n report(\n descriptor.name,\n `holds ${JSON.stringify(value)}, which is not one of ${JSON.stringify(\n descriptor.enumValues ?? [],\n )}`,\n );\n }\n return;\n case 'scalarCollection':\n case 'composition':\n if (!Array.isArray(value)) report(descriptor.name, 'is not a list');\n return;\n default:\n // `string` and `id`, the two that carry text.\n if (typeof value !== 'string' && typeof value !== 'number') {\n report(descriptor.name, `holds a ${typeof value}, not text`);\n }\n }\n}\n\n/**\n * A localized member: an object with non-empty text under every locale. A\n * missing locale fails rather than falls back, so a page never shows one\n * language in the middle of another.\n */\nfunction checkLocalized(\n value: unknown,\n descriptor: EntityFieldDescriptor,\n locales: readonly string[],\n report: Report,\n): void {\n if (!isPlainObject(value)) {\n report(descriptor.name, 'is not a text per locale');\n return;\n }\n for (const locale of locales) {\n const text = value[locale];\n if (text === undefined) {\n report(descriptor.name, `is missing \"${locale}\"`);\n } else if (typeof text !== 'string') {\n report(\n descriptor.name,\n `holds a ${typeof text} for \"${locale}\", not text`,\n );\n } else if (text.trim() === '') {\n report(descriptor.name, `is empty for \"${locale}\"`);\n }\n }\n}\n\n/** `minItems` / `maxItems` on a collection that is present and is a list. */\nfunction checkItemCount(\n value: unknown,\n descriptor: EntityFieldDescriptor,\n report: Report,\n): void {\n if (!Array.isArray(value)) return;\n const { minItems, maxItems } = descriptor;\n if (minItems !== undefined && value.length < minItems) {\n report(\n descriptor.name,\n `holds ${counted(value.length, 'item')}, and needs at least ${minItems}`,\n );\n }\n if (maxItems !== undefined && value.length > maxItems) {\n report(\n descriptor.name,\n `holds ${counted(value.length, 'item')}, and takes at most ${maxItems}`,\n );\n }\n}\n\n/**\n * Checks every record of one entity against its metadata — identity, `required`,\n * each member's type, link ids, item counts, `notBefore`, and the entity's\n * cardinality — then runs the caller's rules.\n */\nexport function validateRecords<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n records: readonly unknown[],\n {\n source,\n linkTargets = {},\n rules = [],\n locales: required = locales(),\n view,\n }: ValidateRecordsOptions = {},\n): ValidateRecordsResult {\n const problems: RecordProblem[] = [];\n const descriptors = describeEntityColumns(entityConstructor);\n const omitted = new Set(\n view === undefined\n ? []\n : extractEntityView(entityConstructor, view).omitted.map(\n descriptor => descriptor.name,\n ),\n );\n const parsed: Record<string, unknown>[] = [];\n const seen = new Set<unknown>();\n\n const at = (where: string, member: string | undefined, message: string) =>\n problems.push({\n path: [source, where, member].filter(Boolean).join(' › '),\n message,\n });\n\n records.forEach((raw, index) => {\n if (!isPlainObject(raw)) {\n at(`#${index}`, undefined, 'is not a record');\n return;\n }\n\n const id = raw['id'];\n const report: Report = (member, message) =>\n at(isAbsent(id) ? `#${index}` : String(id), member, message);\n\n if (isAbsent(id)) {\n report('id', 'is missing');\n } else if (seen.has(id)) {\n report('id', 'is used more than once');\n } else {\n seen.add(id);\n }\n\n const record: Record<string, unknown> = { ...raw };\n\n for (const descriptor of descriptors) {\n if (descriptor.name === 'id') continue;\n const value = raw[descriptor.key];\n\n if (isAbsent(value)) {\n if (descriptor.required && !omitted.has(descriptor.name)) {\n report(descriptor.name, 'is required, and is missing');\n }\n continue;\n }\n\n if (descriptor.type === 'link' || descriptor.type === 'linkCollection') {\n checkLink(value, descriptor, linkTargets[descriptor.name], report);\n } else if (descriptor.type === 'localized') {\n checkLocalized(value, descriptor, required, report);\n } else if (descriptor.type === 'date') {\n const date = parseDate(value, report, descriptor.name);\n if (date !== undefined) record[descriptor.key] = date;\n } else {\n checkScalar(value, descriptor, report);\n }\n\n if (COLLECTION_TYPES.includes(descriptor.type)) {\n checkItemCount(value, descriptor, report);\n }\n }\n\n for (const descriptor of descriptors) {\n if (descriptor.notBefore === undefined) continue;\n const earlier = descriptors.find(\n other => other.name === descriptor.notBefore,\n ) as EntityFieldDescriptor;\n const end = record[descriptor.key];\n const start = record[earlier.key];\n if (start instanceof Date && end instanceof Date && end < start) {\n report(descriptor.name, `is before ${earlier.name}`);\n }\n }\n\n parsed.push(record);\n });\n\n const { cardinality, name } = extractMetaEntity(entityConstructor);\n const where = `${counted(records.length, 'record')}`;\n if (cardinality?.min !== undefined && records.length < cardinality.min) {\n at(name, undefined, `has ${where}, and needs at least ${cardinality.min}`);\n }\n if (cardinality?.max !== undefined && records.length > cardinality.max) {\n at(name, undefined, `has ${where}, and takes at most ${cardinality.max}`);\n }\n\n for (const rule of rules) {\n rule(parsed, (index, member, message) => {\n const id = parsed[index]?.['id'];\n at(isAbsent(id) ? `#${index}` : String(id), member, message);\n });\n }\n\n return { problems, records: parsed };\n}\n\n/** The records with their dates parsed, or a {@link RecordValidationError}. */\nexport function validRecordsOrThrow<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n records: readonly unknown[],\n options?: ValidateRecordsOptions,\n): ReadonlyArray<Record<string, unknown>> {\n const result = validateRecords(entityConstructor, records, options);\n if (result.problems.length > 0) {\n throw new RecordValidationError(result.problems);\n }\n return result.records;\n}\n"],"names":["EntifixBuildError","locales","COLLECTION_TYPES","describeEntityColumns","extractEntityView","extractMetaEntity","RecordValidationError","problems","map","problem","path","message","join","undefined","count","length","isPlainObject","value","Array","isArray","isAbsent","counted","n","noun","parseDate","report","member","Date","Number","isNaN","getTime","parsed","JSON","stringify","checkLink","descriptor","known","isCollection","type","name","id","has","checkScalar","enumValues","includes","checkLocalized","locale","text","trim","checkItemCount","minItems","maxItems","validateRecords","entityConstructor","records","source","linkTargets","rules","required","view","descriptors","omitted","Set","seen","at","where","push","filter","Boolean","forEach","raw","index","String","add","record","key","date","notBefore","earlier","find","other","end","start","cardinality","min","max","rule","validRecordsOrThrow","options","result"],"mappings":"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"}
|
package/dist/i18n/locales.d.ts
CHANGED
|
@@ -1,12 +1,51 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Where a host narrows {@link Locale} to the locales it ships:
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* declare module '@entifix/core' {
|
|
6
|
+
* interface LocaleRegister {
|
|
7
|
+
* locale: 'es' | 'en';
|
|
8
|
+
* }
|
|
9
|
+
* }
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* The same mechanism i18next's `CustomTypeOptions` uses. Without it a locale is
|
|
13
|
+
* any string, which is honest for a framework that does not know which
|
|
14
|
+
* languages its host speaks.
|
|
6
15
|
*/
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
16
|
+
export interface LocaleRegister {
|
|
17
|
+
}
|
|
18
|
+
/** A locale the host declared, or any string when it declared none. */
|
|
19
|
+
export type Locale = LocaleRegister extends {
|
|
20
|
+
locale: infer L extends string;
|
|
21
|
+
} ? L : string;
|
|
22
|
+
/** What a host declares through {@link defineLocales}. */
|
|
23
|
+
export interface LocaleDefinition {
|
|
24
|
+
/** Every locale the host ships, in the order a switcher offers them. */
|
|
25
|
+
readonly locales: readonly Locale[];
|
|
26
|
+
/**
|
|
27
|
+
* The locale a visitor gets when nothing else decides, and the one a missing
|
|
28
|
+
* catalog key falls back to.
|
|
29
|
+
*/
|
|
30
|
+
readonly defaultLocale: Locale;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Declares the host's locales. Call it once, from a module every entry point
|
|
34
|
+
* imports — including a Next middleware, which is bundled on its own and shares
|
|
35
|
+
* no module state with the server.
|
|
36
|
+
*
|
|
37
|
+
* Read at call time by every helper below and by `@entifix/i18n`,
|
|
38
|
+
* `@entifix/react-controls` and `@entifix/next-i18n`, so a host declaring
|
|
39
|
+
* `['en', 'es']` with `en` first gets English as the fallback everywhere, and a
|
|
40
|
+
* third locale needs no change to entifix.
|
|
41
|
+
*/
|
|
42
|
+
export declare function defineLocales(definition: LocaleDefinition): void;
|
|
43
|
+
/** The locales the host declared. */
|
|
44
|
+
export declare function locales(): readonly Locale[];
|
|
45
|
+
/** The host's default locale. */
|
|
46
|
+
export declare function defaultLocale(): Locale;
|
|
47
|
+
/** Forgets the host's declaration. For specs. */
|
|
48
|
+
export declare function resetLocales(): void;
|
|
10
49
|
/**
|
|
11
50
|
* Host-scoped so the choice is shared across the fleet's localhost ports in dev,
|
|
12
51
|
* exactly like `entifix_at`/`entifix_sid`. Written by the locale middleware on every
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../src/i18n/locales.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../src/i18n/locales.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;GAcG;AAGH,MAAM,WAAW,cAAc;CAAG;AAElC,uEAAuE;AACvE,MAAM,MAAM,MAAM,GAAG,cAAc,SAAS;IAAE,MAAM,EAAE,MAAM,CAAC,SAAS,MAAM,CAAA;CAAE,GAC1E,CAAC,GACD,MAAM,CAAC;AAEX,0DAA0D;AAC1D,MAAM,WAAW,gBAAgB;IAC/B,wEAAwE;IACxE,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC;;;OAGG;IACH,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC;AAcD;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,gBAAgB,GAAG,IAAI,CAmBhE;AAED,qCAAqC;AACrC,wBAAgB,OAAO,IAAI,SAAS,MAAM,EAAE,CAE3C;AAED,iCAAiC;AACjC,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,iDAAiD;AACjD,wBAAgB,YAAY,IAAI,IAAI,CAEnC;AAED;;;;GAIG;AACH,eAAO,MAAM,aAAa,mBAAmB,CAAC;AAE9C;;;;GAIG;AACH,eAAO,MAAM,aAAa,qBAAqB,CAAC;AAEhD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAKxD"}
|
package/dist/i18n/locales.js
CHANGED
|
@@ -1,13 +1,52 @@
|
|
|
1
|
+
import { EntifixBuildError } from '../base-entities/entifix-error/index.js';
|
|
1
2
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
* What a host gets when it declares nothing: English alone. Neutral rather than
|
|
4
|
+
* any one adopter's choice — r10c, which once supplied the `es` default here,
|
|
5
|
+
* now declares it like everyone else (#35).
|
|
6
|
+
*/ const UNDECLARED = {
|
|
7
|
+
locales: [
|
|
8
|
+
'en'
|
|
9
|
+
],
|
|
10
|
+
defaultLocale: 'en'
|
|
11
|
+
};
|
|
12
|
+
let declared = UNDECLARED;
|
|
13
|
+
/**
|
|
14
|
+
* Declares the host's locales. Call it once, from a module every entry point
|
|
15
|
+
* imports — including a Next middleware, which is bundled on its own and shares
|
|
16
|
+
* no module state with the server.
|
|
17
|
+
*
|
|
18
|
+
* Read at call time by every helper below and by `@entifix/i18n`,
|
|
19
|
+
* `@entifix/react-controls` and `@entifix/next-i18n`, so a host declaring
|
|
20
|
+
* `['en', 'es']` with `en` first gets English as the fallback everywhere, and a
|
|
21
|
+
* third locale needs no change to entifix.
|
|
22
|
+
*/ export function defineLocales(definition) {
|
|
23
|
+
const { locales: list, defaultLocale: fallback } = definition;
|
|
24
|
+
if (list.length === 0) {
|
|
25
|
+
throw new EntifixBuildError('defineLocales needs at least one locale.');
|
|
26
|
+
}
|
|
27
|
+
const duplicate = list.find((locale, index)=>list.indexOf(locale) !== index);
|
|
28
|
+
if (duplicate !== undefined) {
|
|
29
|
+
throw new EntifixBuildError(`defineLocales was given "${duplicate}" more than once.`);
|
|
30
|
+
}
|
|
31
|
+
if (!list.includes(fallback)) {
|
|
32
|
+
throw new EntifixBuildError(`The default locale "${fallback}" is not one of ${JSON.stringify(list)}.`);
|
|
33
|
+
}
|
|
34
|
+
declared = {
|
|
35
|
+
locales: [
|
|
36
|
+
...list
|
|
37
|
+
],
|
|
38
|
+
defaultLocale: fallback
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** The locales the host declared. */ export function locales() {
|
|
42
|
+
return declared.locales;
|
|
43
|
+
}
|
|
44
|
+
/** The host's default locale. */ export function defaultLocale() {
|
|
45
|
+
return declared.defaultLocale;
|
|
46
|
+
}
|
|
47
|
+
/** Forgets the host's declaration. For specs. */ export function resetLocales() {
|
|
48
|
+
declared = UNDECLARED;
|
|
49
|
+
}
|
|
11
50
|
/**
|
|
12
51
|
* Host-scoped so the choice is shared across the fleet's localhost ports in dev,
|
|
13
52
|
* exactly like `entifix_at`/`entifix_sid`. Written by the locale middleware on every
|
|
@@ -19,7 +58,7 @@ export const DEFAULT_LOCALE = 'es';
|
|
|
19
58
|
* route tree stay free of an `app/[locale]` segment.
|
|
20
59
|
*/ export const LOCALE_HEADER = 'x-entifix-locale';
|
|
21
60
|
export function isLocale(value) {
|
|
22
|
-
return typeof value === 'string' &&
|
|
61
|
+
return typeof value === 'string' && declared.locales.includes(value);
|
|
23
62
|
}
|
|
24
63
|
|
|
25
64
|
//# sourceMappingURL=locales.js.map
|
package/dist/i18n/locales.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/i18n/locales.ts"],"sourcesContent":["/**\n *
|
|
1
|
+
{"version":3,"sources":["../../src/i18n/locales.ts"],"sourcesContent":["import { EntifixBuildError } from '../base-entities/entifix-error/index.js';\n\n/**\n * Where a host narrows {@link Locale} to the locales it ships:\n *\n * ```ts\n * declare module '@entifix/core' {\n * interface LocaleRegister {\n * locale: 'es' | 'en';\n * }\n * }\n * ```\n *\n * The same mechanism i18next's `CustomTypeOptions` uses. Without it a locale is\n * any string, which is honest for a framework that does not know which\n * languages its host speaks.\n */\n// An empty interface is the point: a host merges its declaration into it.\n// eslint-disable-next-line @typescript-eslint/no-empty-interface, @typescript-eslint/no-empty-object-type\nexport interface LocaleRegister {}\n\n/** A locale the host declared, or any string when it declared none. */\nexport type Locale = LocaleRegister extends { locale: infer L extends string }\n ? L\n : string;\n\n/** What a host declares through {@link defineLocales}. */\nexport interface LocaleDefinition {\n /** Every locale the host ships, in the order a switcher offers them. */\n readonly locales: readonly Locale[];\n /**\n * The locale a visitor gets when nothing else decides, and the one a missing\n * catalog key falls back to.\n */\n readonly defaultLocale: Locale;\n}\n\n/**\n * What a host gets when it declares nothing: English alone. Neutral rather than\n * any one adopter's choice — r10c, which once supplied the `es` default here,\n * now declares it like everyone else (#35).\n */\nconst UNDECLARED: LocaleDefinition = {\n locales: ['en' as Locale],\n defaultLocale: 'en' as Locale,\n};\n\nlet declared: LocaleDefinition = UNDECLARED;\n\n/**\n * Declares the host's locales. Call it once, from a module every entry point\n * imports — including a Next middleware, which is bundled on its own and shares\n * no module state with the server.\n *\n * Read at call time by every helper below and by `@entifix/i18n`,\n * `@entifix/react-controls` and `@entifix/next-i18n`, so a host declaring\n * `['en', 'es']` with `en` first gets English as the fallback everywhere, and a\n * third locale needs no change to entifix.\n */\nexport function defineLocales(definition: LocaleDefinition): void {\n const { locales: list, defaultLocale: fallback } = definition;\n if (list.length === 0) {\n throw new EntifixBuildError('defineLocales needs at least one locale.');\n }\n const duplicate = list.find(\n (locale, index) => list.indexOf(locale) !== index,\n );\n if (duplicate !== undefined) {\n throw new EntifixBuildError(\n `defineLocales was given \"${duplicate}\" more than once.`,\n );\n }\n if (!list.includes(fallback)) {\n throw new EntifixBuildError(\n `The default locale \"${fallback}\" is not one of ${JSON.stringify(list)}.`,\n );\n }\n declared = { locales: [...list], defaultLocale: fallback };\n}\n\n/** The locales the host declared. */\nexport function locales(): readonly Locale[] {\n return declared.locales;\n}\n\n/** The host's default locale. */\nexport function defaultLocale(): Locale {\n return declared.defaultLocale;\n}\n\n/** Forgets the host's declaration. For specs. */\nexport function resetLocales(): void {\n declared = UNDECLARED;\n}\n\n/**\n * Host-scoped so the choice is shared across the fleet's localhost ports in dev,\n * exactly like `entifix_at`/`entifix_sid`. Written by the locale middleware on every\n * redirect, so a visitor who lands on `/en/...` keeps English on the next visit.\n */\nexport const LOCALE_COOKIE = 'entifix_locale';\n\n/**\n * Request header the locale middleware sets when it rewrites a prefixed path.\n * Server components read it instead of a route param, which is what lets the\n * route tree stay free of an `app/[locale]` segment.\n */\nexport const LOCALE_HEADER = 'x-entifix-locale';\n\nexport function isLocale(value: unknown): value is Locale {\n return (\n typeof value === 'string' &&\n (declared.locales as readonly string[]).includes(value)\n );\n}\n"],"names":["EntifixBuildError","UNDECLARED","locales","defaultLocale","declared","defineLocales","definition","list","fallback","length","duplicate","find","locale","index","indexOf","undefined","includes","JSON","stringify","resetLocales","LOCALE_COOKIE","LOCALE_HEADER","isLocale","value"],"mappings":"AAAA,SAASA,iBAAiB,QAAQ,0CAA0C;AAqC5E;;;;CAIC,GACD,MAAMC,aAA+B;IACnCC,SAAS;QAAC;KAAe;IACzBC,eAAe;AACjB;AAEA,IAAIC,WAA6BH;AAEjC;;;;;;;;;CASC,GACD,OAAO,SAASI,cAAcC,UAA4B;IACxD,MAAM,EAAEJ,SAASK,IAAI,EAAEJ,eAAeK,QAAQ,EAAE,GAAGF;IACnD,IAAIC,KAAKE,MAAM,KAAK,GAAG;QACrB,MAAM,IAAIT,kBAAkB;IAC9B;IACA,MAAMU,YAAYH,KAAKI,IAAI,CACzB,CAACC,QAAQC,QAAUN,KAAKO,OAAO,CAACF,YAAYC;IAE9C,IAAIH,cAAcK,WAAW;QAC3B,MAAM,IAAIf,kBACR,CAAC,yBAAyB,EAAEU,UAAU,iBAAiB,CAAC;IAE5D;IACA,IAAI,CAACH,KAAKS,QAAQ,CAACR,WAAW;QAC5B,MAAM,IAAIR,kBACR,CAAC,oBAAoB,EAAEQ,SAAS,gBAAgB,EAAES,KAAKC,SAAS,CAACX,MAAM,CAAC,CAAC;IAE7E;IACAH,WAAW;QAAEF,SAAS;eAAIK;SAAK;QAAEJ,eAAeK;IAAS;AAC3D;AAEA,mCAAmC,GACnC,OAAO,SAASN;IACd,OAAOE,SAASF,OAAO;AACzB;AAEA,+BAA+B,GAC/B,OAAO,SAASC;IACd,OAAOC,SAASD,aAAa;AAC/B;AAEA,+CAA+C,GAC/C,OAAO,SAASgB;IACdf,WAAWH;AACb;AAEA;;;;CAIC,GACD,OAAO,MAAMmB,gBAAgB,iBAAiB;AAE9C;;;;CAIC,GACD,OAAO,MAAMC,gBAAgB,mBAAmB;AAEhD,OAAO,SAASC,SAASC,KAAc;IACrC,OACE,OAAOA,UAAU,YACjB,AAACnB,SAASF,OAAO,CAAuBc,QAAQ,CAACO;AAErD"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type Locale } from './locales.js';
|
|
2
|
+
/**
|
|
3
|
+
* One fact in every language the host ships — a product description, a job's
|
|
4
|
+
* responsibilities — held as one value with a text per locale:
|
|
5
|
+
* `{ en: 'Tech Lead', es: 'Líder técnico' }`.
|
|
6
|
+
*
|
|
7
|
+
* The value of a member declared `type: 'localized'`. Content, not UI copy:
|
|
8
|
+
* copy belongs in catalogs, and a record written once per locale duplicates
|
|
9
|
+
* every member that is not text and lets the copies drift.
|
|
10
|
+
*
|
|
11
|
+
* `Partial` because a value read off the wire may be missing a locale; what a
|
|
12
|
+
* complete one must carry is checked by `validateRecords` and by the form,
|
|
13
|
+
* against the locales the host declared.
|
|
14
|
+
*/
|
|
15
|
+
export type LocalizedText = Readonly<Partial<Record<Locale, string>>>;
|
|
16
|
+
/**
|
|
17
|
+
* A localized value in the reader's locale, falling back to the host's default
|
|
18
|
+
* when that locale has no text, and `undefined` when neither does.
|
|
19
|
+
*
|
|
20
|
+
* The caller picks the locale — a page from its route, a cell from the
|
|
21
|
+
* provider — so entities hold every locale and stay locale-agnostic. The
|
|
22
|
+
* fallback is defence, not a plan: validated content carries every locale.
|
|
23
|
+
*/
|
|
24
|
+
export declare function localize(text: LocalizedText | null | undefined, locale: Locale): string | undefined;
|
|
25
|
+
/** Whether a value has the shape of a {@link LocalizedText}: a map of strings. */
|
|
26
|
+
export declare function isLocalizedText(value: unknown): value is LocalizedText;
|
|
27
|
+
//# sourceMappingURL=localized-text.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"localized-text.d.ts","sourceRoot":"","sources":["../../src/i18n/localized-text.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAE1D;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;AAEtE;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CACtB,IAAI,EAAE,aAAa,GAAG,IAAI,GAAG,SAAS,EACtC,MAAM,EAAE,MAAM,GACb,MAAM,GAAG,SAAS,CAIpB;AAED,kFAAkF;AAClF,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,aAAa,CAQtE"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defaultLocale } from './locales.js';
|
|
2
|
+
/**
|
|
3
|
+
* A localized value in the reader's locale, falling back to the host's default
|
|
4
|
+
* when that locale has no text, and `undefined` when neither does.
|
|
5
|
+
*
|
|
6
|
+
* The caller picks the locale — a page from its route, a cell from the
|
|
7
|
+
* provider — so entities hold every locale and stay locale-agnostic. The
|
|
8
|
+
* fallback is defence, not a plan: validated content carries every locale.
|
|
9
|
+
*/ export function localize(text, locale) {
|
|
10
|
+
if (text === null || text === undefined) return undefined;
|
|
11
|
+
const own = text[locale];
|
|
12
|
+
return own !== undefined && own !== '' ? own : text[defaultLocale()];
|
|
13
|
+
}
|
|
14
|
+
/** Whether a value has the shape of a {@link LocalizedText}: a map of strings. */ export function isLocalizedText(value) {
|
|
15
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date) && Object.values(value).every((text)=>typeof text === 'string');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
//# sourceMappingURL=localized-text.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/i18n/localized-text.ts"],"sourcesContent":["import { defaultLocale, type Locale } from './locales.js';\n\n/**\n * One fact in every language the host ships — a product description, a job's\n * responsibilities — held as one value with a text per locale:\n * `{ en: 'Tech Lead', es: 'Líder técnico' }`.\n *\n * The value of a member declared `type: 'localized'`. Content, not UI copy:\n * copy belongs in catalogs, and a record written once per locale duplicates\n * every member that is not text and lets the copies drift.\n *\n * `Partial` because a value read off the wire may be missing a locale; what a\n * complete one must carry is checked by `validateRecords` and by the form,\n * against the locales the host declared.\n */\nexport type LocalizedText = Readonly<Partial<Record<Locale, string>>>;\n\n/**\n * A localized value in the reader's locale, falling back to the host's default\n * when that locale has no text, and `undefined` when neither does.\n *\n * The caller picks the locale — a page from its route, a cell from the\n * provider — so entities hold every locale and stay locale-agnostic. The\n * fallback is defence, not a plan: validated content carries every locale.\n */\nexport function localize(\n text: LocalizedText | null | undefined,\n locale: Locale,\n): string | undefined {\n if (text === null || text === undefined) return undefined;\n const own = text[locale];\n return own !== undefined && own !== '' ? own : text[defaultLocale()];\n}\n\n/** Whether a value has the shape of a {@link LocalizedText}: a map of strings. */\nexport function isLocalizedText(value: unknown): value is LocalizedText {\n return (\n value !== null &&\n typeof value === 'object' &&\n !Array.isArray(value) &&\n !(value instanceof Date) &&\n Object.values(value).every(text => typeof text === 'string')\n );\n}\n"],"names":["defaultLocale","localize","text","locale","undefined","own","isLocalizedText","value","Array","isArray","Date","Object","values","every"],"mappings":"AAAA,SAASA,aAAa,QAAqB,eAAe;AAiB1D;;;;;;;CAOC,GACD,OAAO,SAASC,SACdC,IAAsC,EACtCC,MAAc;IAEd,IAAID,SAAS,QAAQA,SAASE,WAAW,OAAOA;IAChD,MAAMC,MAAMH,IAAI,CAACC,OAAO;IACxB,OAAOE,QAAQD,aAAaC,QAAQ,KAAKA,MAAMH,IAAI,CAACF,gBAAgB;AACtE;AAEA,gFAAgF,GAChF,OAAO,SAASM,gBAAgBC,KAAc;IAC5C,OACEA,UAAU,QACV,OAAOA,UAAU,YACjB,CAACC,MAAMC,OAAO,CAACF,UACf,CAAEA,CAAAA,iBAAiBG,IAAG,KACtBC,OAAOC,MAAM,CAACL,OAAOM,KAAK,CAACX,CAAAA,OAAQ,OAAOA,SAAS;AAEvD"}
|
package/dist/i18n/negotiate.d.ts
CHANGED
|
@@ -32,7 +32,7 @@ export interface NegotiateLocaleInput {
|
|
|
32
32
|
/**
|
|
33
33
|
* The one place the precedence lives: an explicit path prefix beats the visitor's
|
|
34
34
|
* remembered choice, which beats what their browser asks for, which beats the
|
|
35
|
-
*
|
|
35
|
+
* host's default. Pure, so the same function runs in edge middleware, in a server
|
|
36
36
|
* component and in a test.
|
|
37
37
|
*/
|
|
38
38
|
export declare function negotiateLocale({ pathname, cookie, acceptLanguage, }: NegotiateLocaleInput): Locale;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"negotiate.d.ts","sourceRoot":"","sources":["../../src/i18n/negotiate.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"negotiate.d.ts","sourceRoot":"","sources":["../../src/i18n/negotiate.ts"],"names":[],"mappings":"AAAA,OAAO,EAA2B,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAEpE,MAAM,WAAW,UAAU;IACzB,uEAAuE;IACvE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,kEAAkE;IAClE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CAM5D;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAM/D;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAChC,MAAM,GAAG,SAAS,CA4BpB;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzC;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,EAC9B,QAAQ,EACR,MAAM,EACN,cAAc,GACf,EAAE,oBAAoB,GAAG,MAAM,CAO/B"}
|
package/dist/i18n/negotiate.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { defaultLocale, isLocale } from './locales.js';
|
|
2
2
|
/**
|
|
3
3
|
* Splits `/es/catalog/product` into `{ locale: 'es', rest: '/catalog/product' }`.
|
|
4
4
|
* A path whose first segment is not a locale comes back untouched, which is how
|
|
@@ -50,14 +50,14 @@ import { DEFAULT_LOCALE, isLocale } from './locales.js';
|
|
|
50
50
|
/**
|
|
51
51
|
* The one place the precedence lives: an explicit path prefix beats the visitor's
|
|
52
52
|
* remembered choice, which beats what their browser asks for, which beats the
|
|
53
|
-
*
|
|
53
|
+
* host's default. Pure, so the same function runs in edge middleware, in a server
|
|
54
54
|
* component and in a test.
|
|
55
55
|
*/ export function negotiateLocale({ pathname, cookie, acceptLanguage }) {
|
|
56
56
|
var _parseAcceptLanguage;
|
|
57
57
|
const fromPath = pathname === undefined ? undefined : splitLocalePath(pathname).locale;
|
|
58
58
|
if (fromPath !== undefined) return fromPath;
|
|
59
59
|
if (isLocale(cookie)) return cookie;
|
|
60
|
-
return (_parseAcceptLanguage = parseAcceptLanguage(acceptLanguage)) != null ? _parseAcceptLanguage :
|
|
60
|
+
return (_parseAcceptLanguage = parseAcceptLanguage(acceptLanguage)) != null ? _parseAcceptLanguage : defaultLocale();
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
//# sourceMappingURL=negotiate.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/i18n/negotiate.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"sources":["../../src/i18n/negotiate.ts"],"sourcesContent":["import { defaultLocale, isLocale, type Locale } from './locales.js';\n\nexport interface LocalePath {\n /** The locale carried by the path's first segment, if it names one. */\n readonly locale?: Locale;\n /** The path with that segment removed. Always starts with `/`. */\n readonly rest: string;\n}\n\n/**\n * Splits `/es/catalog/product` into `{ locale: 'es', rest: '/catalog/product' }`.\n * A path whose first segment is not a locale comes back untouched, which is how\n * the middleware tells \"needs a redirect\" from \"needs a rewrite\".\n */\nexport function splitLocalePath(pathname: string): LocalePath {\n const [, first = '', ...others] = pathname.split('/');\n if (!isLocale(first)) return { rest: pathname === '' ? '/' : pathname };\n\n const rest = others.join('/');\n return { locale: first, rest: `/${rest}` };\n}\n\n/**\n * Prefixes an in-app href with a locale. Absolute URLs and already-prefixed\n * paths are returned as-is so this is safe to apply blindly at every call site\n * — which is the point, since a single unprefixed `href` would bounce the user\n * through a redirect and lose their place.\n */\nexport function localeHref(locale: Locale, href: string): string {\n if (/^[a-z][a-z0-9+.-]*:|^\\/\\//i.test(href)) return href;\n if (!href.startsWith('/')) return href;\n if (splitLocalePath(href).locale !== undefined) return href;\n\n return href === '/' ? `/${locale}` : `/${locale}${href}`;\n}\n\n/**\n * Picks the best supported locale out of an `Accept-Language` header, honouring\n * q-values and matching `es-419`/`es-MX` onto `es`. Returns `undefined` when the\n * header names nothing we speak, so the caller can fall through to its default.\n */\nexport function parseAcceptLanguage(\n header: string | null | undefined,\n): Locale | undefined {\n if (!header) return undefined;\n\n const ranked = header\n .split(',')\n .map(part => {\n const [tag = '', ...params] = part.trim().split(';');\n // A `q` that is present but unparseable makes the entry malformed rather\n // than top-priority — the `NaN` is filtered out below.\n const q = params.find(param => /^\\s*q=/i.test(param));\n\n return {\n tag: tag.trim().toLowerCase(),\n quality: q === undefined ? 1 : Number(q.trim().slice(2)),\n };\n })\n .filter(\n entry =>\n entry.tag !== '' && !Number.isNaN(entry.quality) && entry.quality > 0,\n )\n .sort((left, right) => right.quality - left.quality);\n\n for (const { tag } of ranked) {\n const base = tag.split('-')[0];\n if (isLocale(base)) return base;\n }\n\n return undefined;\n}\n\nexport interface NegotiateLocaleInput {\n readonly pathname?: string;\n readonly cookie?: string | null;\n readonly acceptLanguage?: string | null;\n}\n\n/**\n * The one place the precedence lives: an explicit path prefix beats the visitor's\n * remembered choice, which beats what their browser asks for, which beats the\n * host's default. Pure, so the same function runs in edge middleware, in a server\n * component and in a test.\n */\nexport function negotiateLocale({\n pathname,\n cookie,\n acceptLanguage,\n}: NegotiateLocaleInput): Locale {\n const fromPath =\n pathname === undefined ? undefined : splitLocalePath(pathname).locale;\n if (fromPath !== undefined) return fromPath;\n if (isLocale(cookie)) return cookie;\n\n return parseAcceptLanguage(acceptLanguage) ?? defaultLocale();\n}\n"],"names":["defaultLocale","isLocale","splitLocalePath","pathname","first","others","split","rest","join","locale","localeHref","href","test","startsWith","undefined","parseAcceptLanguage","header","ranked","map","part","tag","params","trim","q","find","param","toLowerCase","quality","Number","slice","filter","entry","isNaN","sort","left","right","base","negotiateLocale","cookie","acceptLanguage","fromPath"],"mappings":"AAAA,SAASA,aAAa,EAAEC,QAAQ,QAAqB,eAAe;AASpE;;;;CAIC,GACD,OAAO,SAASC,gBAAgBC,QAAgB;IAC9C,MAAM,GAAGC,QAAQ,EAAE,EAAE,GAAGC,OAAO,GAAGF,SAASG,KAAK,CAAC;IACjD,IAAI,CAACL,SAASG,QAAQ,OAAO;QAAEG,MAAMJ,aAAa,KAAK,MAAMA;IAAS;IAEtE,MAAMI,OAAOF,OAAOG,IAAI,CAAC;IACzB,OAAO;QAAEC,QAAQL;QAAOG,MAAM,CAAC,CAAC,EAAEA,MAAM;IAAC;AAC3C;AAEA;;;;;CAKC,GACD,OAAO,SAASG,WAAWD,MAAc,EAAEE,IAAY;IACrD,IAAI,6BAA6BC,IAAI,CAACD,OAAO,OAAOA;IACpD,IAAI,CAACA,KAAKE,UAAU,CAAC,MAAM,OAAOF;IAClC,IAAIT,gBAAgBS,MAAMF,MAAM,KAAKK,WAAW,OAAOH;IAEvD,OAAOA,SAAS,MAAM,CAAC,CAAC,EAAEF,QAAQ,GAAG,CAAC,CAAC,EAAEA,SAASE,MAAM;AAC1D;AAEA;;;;CAIC,GACD,OAAO,SAASI,oBACdC,MAAiC;IAEjC,IAAI,CAACA,QAAQ,OAAOF;IAEpB,MAAMG,SAASD,OACZV,KAAK,CAAC,KACNY,GAAG,CAACC,CAAAA;QACH,MAAM,CAACC,MAAM,EAAE,EAAE,GAAGC,OAAO,GAAGF,KAAKG,IAAI,GAAGhB,KAAK,CAAC;QAChD,yEAAyE;QACzE,uDAAuD;QACvD,MAAMiB,IAAIF,OAAOG,IAAI,CAACC,CAAAA,QAAS,UAAUb,IAAI,CAACa;QAE9C,OAAO;YACLL,KAAKA,IAAIE,IAAI,GAAGI,WAAW;YAC3BC,SAASJ,MAAMT,YAAY,IAAIc,OAAOL,EAAED,IAAI,GAAGO,KAAK,CAAC;QACvD;IACF,GACCC,MAAM,CACLC,CAAAA,QACEA,MAAMX,GAAG,KAAK,MAAM,CAACQ,OAAOI,KAAK,CAACD,MAAMJ,OAAO,KAAKI,MAAMJ,OAAO,GAAG,GAEvEM,IAAI,CAAC,CAACC,MAAMC,QAAUA,MAAMR,OAAO,GAAGO,KAAKP,OAAO;IAErD,KAAK,MAAM,EAAEP,GAAG,EAAE,IAAIH,OAAQ;QAC5B,MAAMmB,OAAOhB,IAAId,KAAK,CAAC,IAAI,CAAC,EAAE;QAC9B,IAAIL,SAASmC,OAAO,OAAOA;IAC7B;IAEA,OAAOtB;AACT;AAQA;;;;;CAKC,GACD,OAAO,SAASuB,gBAAgB,EAC9BlC,QAAQ,EACRmC,MAAM,EACNC,cAAc,EACO;QAMdxB;IALP,MAAMyB,WACJrC,aAAaW,YAAYA,YAAYZ,gBAAgBC,UAAUM,MAAM;IACvE,IAAI+B,aAAa1B,WAAW,OAAO0B;IACnC,IAAIvC,SAASqC,SAAS,OAAOA;IAE7B,QAAOvB,uBAAAA,oBAAoBwB,2BAApBxB,uBAAuCf;AAChD"}
|
package/dist/index.d.ts
CHANGED
|
@@ -20,11 +20,14 @@ export * from './entity-definition/meta-entities/meta-use-case/index.js';
|
|
|
20
20
|
export * from './entity-definition/metadata/index.js';
|
|
21
21
|
export * from './entity-definition/reconstruct/index.js';
|
|
22
22
|
export * from './entity-definition/serializer/index.js';
|
|
23
|
+
export * from './entity-definition/validate/index.js';
|
|
23
24
|
export * from './envelope/index.js';
|
|
24
25
|
export * from './http/session-cookies.js';
|
|
25
26
|
export * from './i18n/format.js';
|
|
26
27
|
export * from './i18n/locales.js';
|
|
28
|
+
export * from './i18n/localized-text.js';
|
|
27
29
|
export * from './i18n/negotiate.js';
|
|
30
|
+
export * from './query/in-memory-query.js';
|
|
28
31
|
export * from './rsql/index.js';
|
|
29
32
|
export * from './types/BulkOutcome.js';
|
|
30
33
|
export * from './types/Entity.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,+CAA+C,CAAC;AAC9D,cAAc,wCAAwC,CAAC;AACvD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,kDAAkD,CAAC;AACjE,cAAc,gDAAgD,CAAC;AAC/D,cAAc,gDAAgD,CAAC;AAC/D,cAAc,kDAAkD,CAAC;AACjE,cAAc,uCAAuC,CAAC;AACtD,cAAc,mCAAmC,CAAC;AAClD,cAAc,sCAAsC,CAAC;AACrD,cAAc,uDAAuD,CAAC;AACtE,cAAc,2DAA2D,CAAC;AAC1E,cAAc,gDAAgD,CAAC;AAC/D,cAAc,uDAAuD,CAAC;AACtE,cAAc,iDAAiD,CAAC;AAChE,cAAc,0DAA0D,CAAC;AACzE,cAAc,wDAAwD,CAAC;AACvE,cAAc,wDAAwD,CAAC;AACvE,cAAc,0DAA0D,CAAC;AACzE,cAAc,uCAAuC,CAAC;AACtD,cAAc,0CAA0C,CAAC;AACzD,cAAc,yCAAyC,CAAC;AACxD,cAAc,qBAAqB,CAAC;AACpC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,yBAAyB,CAAC;AACxC,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,0BAA0B,CAAC;AACzC,cAAc,iBAAiB,CAAC;AAChC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,+CAA+C,CAAC;AAC9D,cAAc,wCAAwC,CAAC;AACvD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,kDAAkD,CAAC;AACjE,cAAc,gDAAgD,CAAC;AAC/D,cAAc,gDAAgD,CAAC;AAC/D,cAAc,kDAAkD,CAAC;AACjE,cAAc,uCAAuC,CAAC;AACtD,cAAc,mCAAmC,CAAC;AAClD,cAAc,sCAAsC,CAAC;AACrD,cAAc,uDAAuD,CAAC;AACtE,cAAc,2DAA2D,CAAC;AAC1E,cAAc,gDAAgD,CAAC;AAC/D,cAAc,uDAAuD,CAAC;AACtE,cAAc,iDAAiD,CAAC;AAChE,cAAc,0DAA0D,CAAC;AACzE,cAAc,wDAAwD,CAAC;AACvE,cAAc,wDAAwD,CAAC;AACvE,cAAc,0DAA0D,CAAC;AACzE,cAAc,uCAAuC,CAAC;AACtD,cAAc,0CAA0C,CAAC;AACzD,cAAc,yCAAyC,CAAC;AACxD,cAAc,uCAAuC,CAAC;AACtD,cAAc,qBAAqB,CAAC;AACpC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,0BAA0B,CAAC;AACzC,cAAc,qBAAqB,CAAC;AACpC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,iBAAiB,CAAC;AAChC,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,yBAAyB,CAAC;AACxC,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,0BAA0B,CAAC;AACzC,cAAc,iBAAiB,CAAC;AAChC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -20,11 +20,14 @@ export * from './entity-definition/meta-entities/meta-use-case/index.js';
|
|
|
20
20
|
export * from './entity-definition/metadata/index.js';
|
|
21
21
|
export * from './entity-definition/reconstruct/index.js';
|
|
22
22
|
export * from './entity-definition/serializer/index.js';
|
|
23
|
+
export * from './entity-definition/validate/index.js';
|
|
23
24
|
export * from './envelope/index.js';
|
|
24
25
|
export * from './http/session-cookies.js';
|
|
25
26
|
export * from './i18n/format.js';
|
|
26
27
|
export * from './i18n/locales.js';
|
|
28
|
+
export * from './i18n/localized-text.js';
|
|
27
29
|
export * from './i18n/negotiate.js';
|
|
30
|
+
export * from './query/in-memory-query.js';
|
|
28
31
|
export * from './rsql/index.js';
|
|
29
32
|
export * from './types/BulkOutcome.js';
|
|
30
33
|
export * from './types/Entity.js';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './base-entities/configuration-client/index.js';\nexport * from './base-entities/entifix-error/index.js';\nexport * from './command-palette/index.js';\nexport * from './entity-definition/decorators/accessor/index.js';\nexport * from './entity-definition/decorators/entity/index.js';\nexport * from './entity-definition/decorators/method/index.js';\nexport * from './entity-definition/decorators/use-case/index.js';\nexport * from './entity-definition/describe/index.js';\nexport * from './entity-definition/field-path.js';\nexport * from './entity-definition/helpers/index.js';\nexport * from './entity-definition/links/apply-entity-links/index.js';\nexport * from './entity-definition/links/entity-collection-link/index.js';\nexport * from './entity-definition/links/entity-link/index.js';\nexport * from './entity-definition/links/entity-link-source/index.js';\nexport * from './entity-definition/links/link-targets/index.js';\nexport * from './entity-definition/meta-entities/meta-accessor/index.js';\nexport * from './entity-definition/meta-entities/meta-entity/index.js';\nexport * from './entity-definition/meta-entities/meta-method/index.js';\nexport * from './entity-definition/meta-entities/meta-use-case/index.js';\nexport * from './entity-definition/metadata/index.js';\nexport * from './entity-definition/reconstruct/index.js';\nexport * from './entity-definition/serializer/index.js';\nexport * from './envelope/index.js';\nexport * from './http/session-cookies.js';\nexport * from './i18n/format.js';\nexport * from './i18n/locales.js';\nexport * from './i18n/negotiate.js';\nexport * from './rsql/index.js';\nexport * from './types/BulkOutcome.js';\nexport * from './types/Entity.js';\nexport * from './types/EntityAction.js';\nexport * from './types/EntityDraft.js';\nexport * from './types/EntityFiltering.js';\nexport * from './types/EntityLoadRequest.js';\nexport * from './types/EntityPage.js';\nexport * from './types/EntityRowDraft.js';\nexport * from './types/EntitySelection.js';\nexport * from './types/EntitySorting.js';\nexport * from './types/Json.js';\nexport * from './types/StandardSchema.js';\nexport * from './types/UseCase.js';\nexport * from './wizard/index.js';\n"],"names":[],"mappings":"AAAA,cAAc,gDAAgD;AAC9D,cAAc,yCAAyC;AACvD,cAAc,6BAA6B;AAC3C,cAAc,mDAAmD;AACjE,cAAc,iDAAiD;AAC/D,cAAc,iDAAiD;AAC/D,cAAc,mDAAmD;AACjE,cAAc,wCAAwC;AACtD,cAAc,oCAAoC;AAClD,cAAc,uCAAuC;AACrD,cAAc,wDAAwD;AACtE,cAAc,4DAA4D;AAC1E,cAAc,iDAAiD;AAC/D,cAAc,wDAAwD;AACtE,cAAc,kDAAkD;AAChE,cAAc,2DAA2D;AACzE,cAAc,yDAAyD;AACvE,cAAc,yDAAyD;AACvE,cAAc,2DAA2D;AACzE,cAAc,wCAAwC;AACtD,cAAc,2CAA2C;AACzD,cAAc,0CAA0C;AACxD,cAAc,sBAAsB;AACpC,cAAc,4BAA4B;AAC1C,cAAc,mBAAmB;AACjC,cAAc,oBAAoB;AAClC,cAAc,sBAAsB;AACpC,cAAc,kBAAkB;AAChC,cAAc,yBAAyB;AACvC,cAAc,oBAAoB;AAClC,cAAc,0BAA0B;AACxC,cAAc,yBAAyB;AACvC,cAAc,6BAA6B;AAC3C,cAAc,+BAA+B;AAC7C,cAAc,wBAAwB;AACtC,cAAc,4BAA4B;AAC1C,cAAc,6BAA6B;AAC3C,cAAc,2BAA2B;AACzC,cAAc,kBAAkB;AAChC,cAAc,4BAA4B;AAC1C,cAAc,qBAAqB;AACnC,cAAc,oBAAoB"}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './base-entities/configuration-client/index.js';\nexport * from './base-entities/entifix-error/index.js';\nexport * from './command-palette/index.js';\nexport * from './entity-definition/decorators/accessor/index.js';\nexport * from './entity-definition/decorators/entity/index.js';\nexport * from './entity-definition/decorators/method/index.js';\nexport * from './entity-definition/decorators/use-case/index.js';\nexport * from './entity-definition/describe/index.js';\nexport * from './entity-definition/field-path.js';\nexport * from './entity-definition/helpers/index.js';\nexport * from './entity-definition/links/apply-entity-links/index.js';\nexport * from './entity-definition/links/entity-collection-link/index.js';\nexport * from './entity-definition/links/entity-link/index.js';\nexport * from './entity-definition/links/entity-link-source/index.js';\nexport * from './entity-definition/links/link-targets/index.js';\nexport * from './entity-definition/meta-entities/meta-accessor/index.js';\nexport * from './entity-definition/meta-entities/meta-entity/index.js';\nexport * from './entity-definition/meta-entities/meta-method/index.js';\nexport * from './entity-definition/meta-entities/meta-use-case/index.js';\nexport * from './entity-definition/metadata/index.js';\nexport * from './entity-definition/reconstruct/index.js';\nexport * from './entity-definition/serializer/index.js';\nexport * from './entity-definition/validate/index.js';\nexport * from './envelope/index.js';\nexport * from './http/session-cookies.js';\nexport * from './i18n/format.js';\nexport * from './i18n/locales.js';\nexport * from './i18n/localized-text.js';\nexport * from './i18n/negotiate.js';\nexport * from './query/in-memory-query.js';\nexport * from './rsql/index.js';\nexport * from './types/BulkOutcome.js';\nexport * from './types/Entity.js';\nexport * from './types/EntityAction.js';\nexport * from './types/EntityDraft.js';\nexport * from './types/EntityFiltering.js';\nexport * from './types/EntityLoadRequest.js';\nexport * from './types/EntityPage.js';\nexport * from './types/EntityRowDraft.js';\nexport * from './types/EntitySelection.js';\nexport * from './types/EntitySorting.js';\nexport * from './types/Json.js';\nexport * from './types/StandardSchema.js';\nexport * from './types/UseCase.js';\nexport * from './wizard/index.js';\n"],"names":[],"mappings":"AAAA,cAAc,gDAAgD;AAC9D,cAAc,yCAAyC;AACvD,cAAc,6BAA6B;AAC3C,cAAc,mDAAmD;AACjE,cAAc,iDAAiD;AAC/D,cAAc,iDAAiD;AAC/D,cAAc,mDAAmD;AACjE,cAAc,wCAAwC;AACtD,cAAc,oCAAoC;AAClD,cAAc,uCAAuC;AACrD,cAAc,wDAAwD;AACtE,cAAc,4DAA4D;AAC1E,cAAc,iDAAiD;AAC/D,cAAc,wDAAwD;AACtE,cAAc,kDAAkD;AAChE,cAAc,2DAA2D;AACzE,cAAc,yDAAyD;AACvE,cAAc,yDAAyD;AACvE,cAAc,2DAA2D;AACzE,cAAc,wCAAwC;AACtD,cAAc,2CAA2C;AACzD,cAAc,0CAA0C;AACxD,cAAc,wCAAwC;AACtD,cAAc,sBAAsB;AACpC,cAAc,4BAA4B;AAC1C,cAAc,mBAAmB;AACjC,cAAc,oBAAoB;AAClC,cAAc,2BAA2B;AACzC,cAAc,sBAAsB;AACpC,cAAc,6BAA6B;AAC3C,cAAc,kBAAkB;AAChC,cAAc,yBAAyB;AACvC,cAAc,oBAAoB;AAClC,cAAc,0BAA0B;AACxC,cAAc,yBAAyB;AACvC,cAAc,6BAA6B;AAC3C,cAAc,+BAA+B;AAC7C,cAAc,wBAAwB;AACtC,cAAc,4BAA4B;AAC1C,cAAc,6BAA6B;AAC3C,cAAc,2BAA2B;AACzC,cAAc,kBAAkB;AAChC,cAAc,4BAA4B;AAC1C,cAAc,qBAAqB;AACnC,cAAc,oBAAoB"}
|