@mkja/o-data 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/filter.js ADDED
@@ -0,0 +1,278 @@
1
+ // ============================================================================
2
+ // Filter Types
3
+ // ============================================================================
4
+ import { buildQueryableEntity } from './runtime.js';
5
+ // ============================================================================
6
+ // Filter Builder Runtime Implementation
7
+ // ============================================================================
8
+ class FilterBuilderImpl {
9
+ state;
10
+ constructor(initialState) {
11
+ this.state = initialState;
12
+ }
13
+ and(expr) {
14
+ return new FilterBuilderImpl([...this.state, 'and', expr.state]);
15
+ }
16
+ or(expr) {
17
+ return new FilterBuilderImpl([...this.state, 'or', expr.state]);
18
+ }
19
+ __brand = 'FilterBuilder';
20
+ }
21
+ export function createFilterHelpers(entityDef, schema) {
22
+ const clause = (property, operator, value) => {
23
+ return new FilterBuilderImpl([[property, operator, value]]);
24
+ };
25
+ // Helper to recursively update paths in the builder state
26
+ const prependPathToState = (state, prefix) => {
27
+ return state.map((item) => {
28
+ if (Array.isArray(item)) {
29
+ // Check if it's a clause tuple [property, operator, value]
30
+ const ops = [
31
+ 'eq',
32
+ 'ne',
33
+ 'gt',
34
+ 'ge',
35
+ 'lt',
36
+ 'le',
37
+ 'contains',
38
+ 'startswith',
39
+ 'endswith',
40
+ 'in',
41
+ ];
42
+ if (item.length === 3 &&
43
+ typeof item[0] === 'string' &&
44
+ typeof item[1] === 'string' &&
45
+ ops.includes(item[1])) {
46
+ return [`${prefix}/${item[0]}`, item[1], item[2]];
47
+ }
48
+ else {
49
+ return prependPathToState(item, prefix);
50
+ }
51
+ }
52
+ else if (typeof item === 'object' && item !== null && item.kind === 'lambda') {
53
+ // Update lambda navigation path
54
+ return { ...item, nav: `${prefix}/${item.nav}` };
55
+ }
56
+ return item;
57
+ });
58
+ };
59
+ const nav = (navKey, cb) => {
60
+ const navDef = entityDef.navigations[navKey];
61
+ if (!navDef) {
62
+ throw new Error(`Navigation ${String(navKey)} not found`);
63
+ }
64
+ // At runtime, target is a string (entitytype name), need to resolve to QueryableEntity
65
+ if (!schema) {
66
+ throw new Error('Schema required for navigation filters');
67
+ }
68
+ const targetEntitytypeName = navDef.target;
69
+ const targetEntitysetKey = navDef.targetEntitysetKey;
70
+ const targetEntity = buildQueryableEntity(schema, targetEntitysetKey);
71
+ const innerHelpers = createFilterHelpers(targetEntity, schema);
72
+ const innerBuilder = cb(innerHelpers);
73
+ // Transform the inner state by prepending the navigation key
74
+ const innerState = innerBuilder.state;
75
+ const scopedState = prependPathToState(innerState, String(navKey));
76
+ return new FilterBuilderImpl(scopedState);
77
+ };
78
+ const any = (nav, cb) => {
79
+ const navDef = entityDef.navigations[nav];
80
+ if (!navDef) {
81
+ throw new Error(`Navigation ${String(nav)} not found`);
82
+ }
83
+ if (!schema) {
84
+ throw new Error('Schema required for navigation filters');
85
+ }
86
+ const targetEntitysetKey = navDef.targetEntitysetKey;
87
+ const targetEntity = buildQueryableEntity(schema, targetEntitysetKey);
88
+ const innerHelpers = createFilterHelpers(targetEntity, schema);
89
+ const innerBuilder = cb(innerHelpers);
90
+ const lambdaState = {
91
+ kind: 'lambda',
92
+ op: 'any',
93
+ nav: nav,
94
+ predicate: innerBuilder.state,
95
+ };
96
+ return new FilterBuilderImpl([lambdaState]);
97
+ };
98
+ const all = (nav, cb) => {
99
+ const navDef = entityDef.navigations[nav];
100
+ if (!navDef) {
101
+ throw new Error(`Navigation ${String(nav)} not found`);
102
+ }
103
+ if (!schema) {
104
+ throw new Error('Schema required for navigation filters');
105
+ }
106
+ const targetEntitysetKey = navDef.targetEntitysetKey;
107
+ const targetEntity = buildQueryableEntity(schema, targetEntitysetKey);
108
+ const innerHelpers = createFilterHelpers(targetEntity, schema);
109
+ const innerBuilder = cb(innerHelpers);
110
+ const lambdaState = {
111
+ kind: 'lambda',
112
+ op: 'all',
113
+ nav: nav,
114
+ predicate: innerBuilder.state,
115
+ };
116
+ return new FilterBuilderImpl([lambdaState]);
117
+ };
118
+ return { clause, nav, any, all };
119
+ }
120
+ // ============================================================================
121
+ // Filter Serialization
122
+ // ============================================================================
123
+ export function serializeFilter(filterState, depth = 0, lambdaVar, entityDef, schema) {
124
+ if (filterState.length === 0) {
125
+ return '';
126
+ }
127
+ // Handle lambda
128
+ if (filterState.length === 1 &&
129
+ typeof filterState[0] === 'object' &&
130
+ filterState[0] !== null &&
131
+ filterState[0].kind === 'lambda') {
132
+ const lambda = filterState[0];
133
+ const varName = lambdaVar || `p${depth}`;
134
+ let lambdaEntityDef;
135
+ if (entityDef && lambda.nav in entityDef.navigations) {
136
+ // For lambda navigation, we need to resolve the target entity
137
+ // The nav might be a path (e.g., A/B/C), so we take the first part
138
+ const firstPart = lambda.nav.split('/')[0];
139
+ const nav = entityDef.navigations[firstPart];
140
+ if (nav) {
141
+ // At runtime, target is a string, but we don't have schema here
142
+ // We'll pass undefined and let serializeClause handle it
143
+ lambdaEntityDef = undefined;
144
+ }
145
+ }
146
+ const predicate = serializeFilter(lambda.predicate, depth + 1, varName, lambdaEntityDef, schema);
147
+ return `${lambda.nav}/${lambda.op}(${varName}:${predicate})`;
148
+ }
149
+ // Handle clause
150
+ if (filterState.length === 3 && typeof filterState[0] === 'string') {
151
+ const [property, operator, value] = filterState;
152
+ const qualifiedProperty = lambdaVar ? `${lambdaVar}/${property}` : property;
153
+ return serializeClause(qualifiedProperty, operator, value, entityDef, property, schema);
154
+ }
155
+ // Handle logical operators
156
+ let result = '';
157
+ let i = 0;
158
+ while (i < filterState.length) {
159
+ const part = filterState[i];
160
+ if (part === 'and' || part === 'or') {
161
+ const operator = part;
162
+ i++;
163
+ if (i < filterState.length) {
164
+ const right = serializeFilter(Array.isArray(filterState[i]) ? filterState[i] : [filterState[i]], depth, lambdaVar, entityDef, schema);
165
+ result = `(${result}) ${operator} (${right})`;
166
+ }
167
+ }
168
+ else {
169
+ const partStr = serializeFilter(Array.isArray(part) ? part : [part], depth, lambdaVar, entityDef, schema);
170
+ result = result ? `${result} ${partStr}` : partStr;
171
+ }
172
+ i++;
173
+ }
174
+ return result;
175
+ }
176
+ // Helper function to resolve enum member name from numeric value
177
+ function resolveEnumMemberName(schema, enumTypeName, value) {
178
+ if (!schema.enumtypes) {
179
+ return undefined;
180
+ }
181
+ const enumType = schema.enumtypes[enumTypeName];
182
+ if (!enumType || !enumType.members) {
183
+ return undefined;
184
+ }
185
+ const entry = Object.entries(enumType.members).find(([_, val]) => val === value);
186
+ return entry ? entry[0] : undefined;
187
+ }
188
+ // Helper function to format enum value as FQN
189
+ function formatEnumValue(schema, enumTypeName, memberName) {
190
+ if (!schema || !schema.namespace) {
191
+ // Fallback to plain member name if schema/namespace not available
192
+ return `'${memberName}'`;
193
+ }
194
+ return `${schema.namespace}.${enumTypeName}'${memberName}'`;
195
+ }
196
+ function serializeClause(property, operator, value, entityDef, originalProperty, schema) {
197
+ // Check if this is an enum property
198
+ const isEnumProperty = () => {
199
+ if (!originalProperty || !entityDef || !entityDef.properties) {
200
+ return { isEnum: false };
201
+ }
202
+ const propDef = entityDef.properties[originalProperty];
203
+ if (propDef && typeof propDef === 'object' && 'type' in propDef && propDef.type === 'enum') {
204
+ const enumTypeName = propDef.target;
205
+ return { isEnum: true, enumTypeName };
206
+ }
207
+ return { isEnum: false };
208
+ };
209
+ const enumInfo = isEnumProperty();
210
+ const formatValue = (val) => {
211
+ if (val === null || val === undefined) {
212
+ return 'null';
213
+ }
214
+ // Handle enum values
215
+ if (enumInfo.isEnum && enumInfo.enumTypeName && schema) {
216
+ let memberName;
217
+ if (typeof val === 'string') {
218
+ // Use string value as member name
219
+ memberName = val;
220
+ }
221
+ else if (typeof val === 'number') {
222
+ // Look up member name from numeric value
223
+ const resolved = resolveEnumMemberName(schema, enumInfo.enumTypeName, val);
224
+ if (!resolved) {
225
+ // Fallback to number if member not found
226
+ return String(val);
227
+ }
228
+ memberName = resolved;
229
+ }
230
+ else {
231
+ // Not a string or number, fall through to normal formatting
232
+ return formatValueNonEnum(val);
233
+ }
234
+ return formatEnumValue(schema, enumInfo.enumTypeName, memberName);
235
+ }
236
+ return formatValueNonEnum(val);
237
+ };
238
+ const formatValueNonEnum = (val) => {
239
+ if (val === null || val === undefined) {
240
+ return 'null';
241
+ }
242
+ // Handle Date values
243
+ if (val instanceof Date || (typeof val === 'string' && /^\d{4}-\d{2}-\d{2}/.test(val))) {
244
+ let dateValue;
245
+ if (val instanceof Date) {
246
+ dateValue = val;
247
+ }
248
+ else {
249
+ dateValue = new Date(val);
250
+ }
251
+ // Default to ISO format (DateTimeOffset)
252
+ return dateValue.toISOString();
253
+ }
254
+ // Handle strings
255
+ if (typeof val === 'string') {
256
+ return `'${val.replace(/'/g, "''")}'`;
257
+ }
258
+ // Handle arrays (for 'in' operator)
259
+ if (Array.isArray(val)) {
260
+ return `(${val.map(formatValue).join(',')})`;
261
+ }
262
+ // Handle numbers and booleans
263
+ return String(val);
264
+ };
265
+ // Handle string functions
266
+ if (operator === 'contains' || operator === 'startswith' || operator === 'endswith') {
267
+ return `${operator}(${property},${formatValue(value)})`;
268
+ }
269
+ // Handle 'in' operator
270
+ if (operator === 'in') {
271
+ if (!Array.isArray(value)) {
272
+ throw new Error(`'in' operator requires an array value`);
273
+ }
274
+ return `${property} in ${formatValue(value)}`;
275
+ }
276
+ // Standard comparison operators
277
+ return `${property} ${operator} ${formatValue(value)}`;
278
+ }
@@ -0,0 +1,101 @@
1
+ import type { Schema } from './schema';
2
+ import type { QueryableEntity, EntitySetToQueryableEntity, EntitySetToQueryableEntity as ResolveEntitySet, ImportedActionKeys, ImportedFunctionKeys, ResolveActionFromImport, ResolveFunctionFromImport, BoundActionKeysForEntitySet, BoundFunctionKeysForEntitySet } from './types';
3
+ import type { CollectionQueryResponse, SingleQueryResponse, CreateResponse, UpdateResponse, DeleteResponse, ActionResponse, FunctionResponse } from './response';
4
+ import type { CollectionQueryObject, SingleQueryObject, QueryOperationOptions } from './query';
5
+ import type { CreateObject, UpdateObject, CreateOperationOptions, UpdateOperationOptions, OperationParameters } from './operations';
6
+ type Fetch = (input: Request, init?: RequestInit) => Promise<Response>;
7
+ export type OdataClientOptions = {
8
+ baseUrl: string;
9
+ transport: Fetch;
10
+ };
11
+ type EntitySetNames<S extends Schema<S>> = keyof S['entitysets'];
12
+ type EntitySetToQE<S extends Schema<S>, ES extends EntitySetNames<S>> = EntitySetToQueryableEntity<S, ES>;
13
+ export declare class OdataClient<S extends Schema<S>> {
14
+ #private;
15
+ constructor(schema: S, options: OdataClientOptions);
16
+ /**
17
+ * Access an entityset collection.
18
+ */
19
+ entitysets<E extends EntitySetNames<S>>(entityset: E): CollectionOperation<S, EntitySetToQE<S, E>, E>;
20
+ /**
21
+ * Execute an unbound global action.
22
+ */
23
+ action<A extends ImportedActionKeys<S>>(name: A, payload: {
24
+ parameters: OperationParameters<S, NonNullable<S['actions']>[ResolveActionFromImport<S, A>]['parameters']>;
25
+ }): Promise<ActionResponse<S, NonNullable<S['actions']>[ResolveActionFromImport<S, A>]['returnType']>>;
26
+ /**
27
+ * Execute an unbound global function.
28
+ */
29
+ function<F extends ImportedFunctionKeys<S>>(name: F, payload: {
30
+ parameters: OperationParameters<S, NonNullable<S['functions']>[ResolveFunctionFromImport<S, F>]['parameters']>;
31
+ }): Promise<FunctionResponse<S, NonNullable<S['functions']>[ResolveFunctionFromImport<S, F>]['returnType']>>;
32
+ }
33
+ declare class CollectionOperation<S extends Schema<S>, QE extends QueryableEntity, E extends EntitySetNames<S> = EntitySetNames<S>> {
34
+ #private;
35
+ constructor(schema: S, entityset: QE, entitysetName: E, path: string, options: OdataClientOptions);
36
+ /**
37
+ * Query a collection of entities.
38
+ */
39
+ query<Q extends CollectionQueryObject<QE, S>, O extends QueryOperationOptions>(q: Q, o?: O): Promise<CollectionQueryResponse<QE, Q, O>>;
40
+ /**
41
+ * Build the full URL for this operation.
42
+ */
43
+ private buildUrl;
44
+ /**
45
+ * Create a new entity.
46
+ */
47
+ create<O extends CreateOperationOptions<QE>>(c: CreateObject<QE>, o?: O): Promise<CreateResponse<QE, O>>;
48
+ /**
49
+ * Access a single entity by key.
50
+ */
51
+ key(key: string): SingleOperation<S, QE, E>;
52
+ /**
53
+ * Execute a bound action on the collection.
54
+ */
55
+ action<K extends BoundActionKeysForEntitySet<S, E, 'collection'>>(name: K, payload: {
56
+ parameters: OperationParameters<S, NonNullable<S['actions']>[K]['parameters']>;
57
+ }): Promise<ActionResponse<S, NonNullable<S['actions']>[K]['returnType']>>;
58
+ /**
59
+ * Execute a bound function on the collection.
60
+ */
61
+ function<K extends BoundFunctionKeysForEntitySet<S, E, 'collection'>>(name: K, payload: {
62
+ parameters: OperationParameters<S, NonNullable<S['functions']>[K]['parameters']>;
63
+ }): Promise<FunctionResponse<S, NonNullable<S['functions']>[K]['returnType']>>;
64
+ }
65
+ declare class SingleOperation<S extends Schema<S>, QE extends QueryableEntity, E extends EntitySetNames<S> = EntitySetNames<S>> {
66
+ #private;
67
+ constructor(schema: S, entityset: QE, entitysetName: E, path: string, options: OdataClientOptions);
68
+ /**
69
+ * Query a single entity.
70
+ */
71
+ query<Q extends SingleQueryObject<QE, S>, O extends QueryOperationOptions>(q: Q, o?: O): Promise<SingleQueryResponse<QE, Q, O>>;
72
+ /**
73
+ * Build the full URL for this operation.
74
+ */
75
+ private buildUrl;
76
+ /**
77
+ * Update an entity.
78
+ */
79
+ update<O extends UpdateOperationOptions<QE>>(u: UpdateObject<QE>, o?: O): Promise<UpdateResponse<QE, O>>;
80
+ /**
81
+ * Delete an entity.
82
+ */
83
+ delete(): Promise<DeleteResponse>;
84
+ /**
85
+ * Navigate to a related entity or collection.
86
+ */
87
+ navigate<N extends keyof QE['navigations']>(navigation_property: N): QE['navigations'][N]['targetEntitysetKey'] extends string ? QE['navigations'][N]['collection'] extends true ? CollectionOperation<S, ResolveEntitySet<S, QE['navigations'][N]['targetEntitysetKey']>> : SingleOperation<S, ResolveEntitySet<S, QE['navigations'][N]['targetEntitysetKey']>> : QE['navigations'][N]['collection'] extends true ? CollectionOperation<S, QueryableEntity> : SingleOperation<S, QueryableEntity>;
88
+ /**
89
+ * Execute a bound action on the entity.
90
+ */
91
+ action<K extends BoundActionKeysForEntitySet<S, E, 'entity'>>(name: K, payload: {
92
+ parameters: OperationParameters<S, NonNullable<S['actions']>[K]['parameters']>;
93
+ }): Promise<ActionResponse<S, NonNullable<S['actions']>[K]['returnType']>>;
94
+ /**
95
+ * Execute a bound function on the entity.
96
+ */
97
+ function<K extends BoundFunctionKeysForEntitySet<S, E, 'entity'>>(name: K, payload: {
98
+ parameters: OperationParameters<S, NonNullable<S['functions']>[K]['parameters']>;
99
+ }): Promise<FunctionResponse<S, NonNullable<S['functions']>[K]['returnType']>>;
100
+ }
101
+ export {};