@simtlix/simfinity-js-client 1.1.0 → 3.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -5
- package/package.json +18 -6
- package/src/SimfinityClient.js +231 -393
- package/src/inputs.js +106 -0
- package/src/metadata.js +104 -0
- package/src/selection.js +88 -0
- package/types/index.d.ts +91 -0
- package/.claude/worktrees/flamboyant-cartwright/.claude/settings.local.json +0 -18
- package/.claude/worktrees/flamboyant-cartwright/IMPLEMENTATION_PROMPT.md +0 -1137
- package/.claude/worktrees/flamboyant-cartwright/LICENSE +0 -201
- package/.claude/worktrees/flamboyant-cartwright/README.md +0 -491
- package/.claude/worktrees/flamboyant-cartwright/examples/demo.js +0 -402
- package/.claude/worktrees/flamboyant-cartwright/package-lock.json +0 -107
- package/.claude/worktrees/flamboyant-cartwright/package.json +0 -29
- package/.claude/worktrees/flamboyant-cartwright/src/SimfinityClient.js +0 -1398
- package/IMPLEMENTATION_PROMPT.md +0 -1137
- package/examples/demo.js +0 -402
- package/simtlix-simfinity-js-client-1.0.0.tgz +0 -0
- package/simtlix-simfinity-js-client-1.0.1.tgz +0 -0
- package/simtlix-simfinity-js-client-1.0.3.tgz +0 -0
- package/simtlix-simfinity-js-client-1.0.4.tgz +0 -0
- package/simtlix-simfinity-js-client-1.0.5.tgz +0 -0
package/src/inputs.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
const leaf = (ref) => { let current = ref; while (current?.ofType) current = current.ofType; return current; };
|
|
2
|
+
const present = (value, options) => value !== undefined && (options.preserveEmpty || (value !== null && value !== ''));
|
|
3
|
+
const versionValue = (value) => {
|
|
4
|
+
if (!Number.isSafeInteger(value) || value < 1) throw new Error('Record version must be a JSON integer from 1 to 9007199254740991');
|
|
5
|
+
return value;
|
|
6
|
+
};
|
|
7
|
+
const deltaType = (type) => type?.kind === 'INPUT_OBJECT' && type.fields.length > 0 && type.fields.every((field) => ['added', 'updated', 'deleted'].includes(field.name) && field.type.isList);
|
|
8
|
+
|
|
9
|
+
export const cleanInputValue = (client, ref, value, options = {}, root = false) => {
|
|
10
|
+
if (ref.kind === 'NON_NULL') return cleanInputValue(client, ref.ofType, value, options, root);
|
|
11
|
+
if (value === undefined || value === null) return value;
|
|
12
|
+
if (ref.kind === 'LIST') return Array.isArray(value) ? value.map((item) => cleanInputValue(client, ref.ofType, item, options)) : cleanInputValue(client, ref.ofType, value, options);
|
|
13
|
+
if (ref.kind !== 'INPUT_OBJECT') {
|
|
14
|
+
if (ref.name === 'RecordVersion') return versionValue(value);
|
|
15
|
+
if (ref.kind === 'ENUM' || (options.preserveEmpty && value === '')) return value;
|
|
16
|
+
return client._coerceScalarValue(value, client._types.get(ref.name)?.baseScalar || ref.name);
|
|
17
|
+
}
|
|
18
|
+
const type = client._types.get(ref.name);
|
|
19
|
+
if (!type) throw new Error(`Unknown input type ${ref.name}`);
|
|
20
|
+
const input = typeof value === 'string' && type.fields.some((f) => f.name === 'id') ? { id: value } : value;
|
|
21
|
+
if (typeof input !== 'object' || Array.isArray(input)) throw new Error(`Expected input object ${ref.name}`);
|
|
22
|
+
const protectedVersion = type.fields.find((f) => f.name === 'version' && leaf(f.rawType)?.name === 'RecordVersion');
|
|
23
|
+
if (protectedVersion) versionValue(input.version);
|
|
24
|
+
const output = {};
|
|
25
|
+
for (const field of type.fields) {
|
|
26
|
+
const name = field.name;
|
|
27
|
+
if (input[name] === undefined || options.skipFields?.includes(name) || options.transientFields?.includes(name)) continue;
|
|
28
|
+
if (root && name === 'id' && options.mode !== 'update') continue;
|
|
29
|
+
if (name === '__typename' || name === '__status' || name === '__originalData') continue;
|
|
30
|
+
const target = client._types.get(leaf(field.rawType)?.name);
|
|
31
|
+
if (deltaType(target) && !options.includeCollections) continue;
|
|
32
|
+
if (field.type.isList && field.type.kind === 'INPUT_OBJECT' && !deltaType(type) && !options.includeCollections) continue;
|
|
33
|
+
let fieldValue = input[name];
|
|
34
|
+
if (deltaType(type) && name === 'deleted' && leaf(field.rawType)?.name === 'ID' && Array.isArray(fieldValue)) {
|
|
35
|
+
fieldValue = fieldValue.map((item) => item !== null && typeof item === 'object' ? item.id : item);
|
|
36
|
+
}
|
|
37
|
+
const cleaned = cleanInputValue(client, field.rawType, fieldValue, options);
|
|
38
|
+
if (present(cleaned, options)) output[name] = cleaned;
|
|
39
|
+
}
|
|
40
|
+
return output;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const transformRootInput = (client, typeName, raw, options = {}) => {
|
|
44
|
+
const mode = options.mode || 'create';
|
|
45
|
+
const inputName = options.inputType || client._inputTypeName(typeName, mode === 'create' ? 'add' : 'update');
|
|
46
|
+
if (!inputName) return client._legacyTransformInput(typeName, raw, options);
|
|
47
|
+
const type = client._types.get(inputName);
|
|
48
|
+
if (!type || type.kind !== 'INPUT_OBJECT') throw new Error(`Unknown input type ${inputName}`);
|
|
49
|
+
if (mode === 'update' && raw.version !== undefined && !type.fields.some((f) => f.name === 'version')) throw new Error(`Input ${inputName} does not accept a version`);
|
|
50
|
+
return cleanInputValue(client, { name: inputName, kind: 'INPUT_OBJECT' }, raw, { ...options, mode }, true);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const collectionItemInput = (client, targetName, mode) => {
|
|
54
|
+
const kind = mode === 'create' ? 'add' : 'update';
|
|
55
|
+
const roots = [...client._mutations.values()].filter((operation) => ['add', 'update', 'stateTransition'].includes(operation.category));
|
|
56
|
+
const direct = roots.find((operation) => operation.typeName === targetName && operation.category === kind);
|
|
57
|
+
if (direct) return leaf(direct.args.find((arg) => arg.name === 'input')?.type)?.name;
|
|
58
|
+
const candidates = new Set(); const seen = new Set();
|
|
59
|
+
const visit = (entityName, inputName) => {
|
|
60
|
+
const key = `${entityName}:${inputName}`; if (seen.has(key)) return; seen.add(key);
|
|
61
|
+
const entity = client._types.get(entityName); const input = client._types.get(inputName);
|
|
62
|
+
if (!entity || input?.kind !== 'INPUT_OBJECT') return;
|
|
63
|
+
for (const field of input.fields) {
|
|
64
|
+
const output = entity.fields.find((item) => item.name === field.name);
|
|
65
|
+
if (output?.type.kind !== 'OBJECT') continue;
|
|
66
|
+
const nested = client._types.get(field.type.name);
|
|
67
|
+
if (output.type.isList && deltaType(nested)) {
|
|
68
|
+
for (const entry of nested.fields.filter((item) => ['added', 'updated'].includes(item.name))) {
|
|
69
|
+
if (output.type.name === targetName && entry.name === (mode === 'create' ? 'added' : 'updated')) candidates.add(entry.type.name);
|
|
70
|
+
visit(output.type.name, entry.type.name);
|
|
71
|
+
}
|
|
72
|
+
} else visit(output.type.name, field.type.name);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
for (const root of roots) visit(root.typeName, leaf(root.args.find((arg) => arg.name === 'input')?.type)?.name);
|
|
76
|
+
if (candidates.size !== 1) throw new Error('Cannot choose a collection input unambiguously; supply inputType');
|
|
77
|
+
return [...candidates][0];
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const transformDelta = (client, typeName, delta, options = {}) => {
|
|
81
|
+
if (options.inputType) {
|
|
82
|
+
const input = client._types.get(options.inputType);
|
|
83
|
+
if (!deltaType(input)) throw new Error(`Not a collection delta input: ${options.inputType}`);
|
|
84
|
+
return cleanInputValue(client, { kind: 'INPUT_OBJECT', name: options.inputType }, delta, { ...options, includeCollections: true });
|
|
85
|
+
}
|
|
86
|
+
const result = {};
|
|
87
|
+
const clean = (item, mode) => {
|
|
88
|
+
const raw = { ...item };
|
|
89
|
+
delete raw.__status; delete raw.__originalData; delete raw.__typename;
|
|
90
|
+
if (options.connectionField) delete raw[options.connectionField];
|
|
91
|
+
if (mode === 'create' && typeof raw.id === 'string' && raw.id.startsWith('temp_')) delete raw.id;
|
|
92
|
+
return transformRootInput(client, typeName, raw, { ...options, mode, inputType: collectionItemInput(client, typeName, mode), includeCollections: true });
|
|
93
|
+
};
|
|
94
|
+
if (delta.added?.length) result.added = delta.added.map((item) => clean(item, 'create'));
|
|
95
|
+
if (delta.updated?.length) result.updated = delta.updated.map((item) => clean(item, 'update'));
|
|
96
|
+
if (delta.deleted?.length) {
|
|
97
|
+
const protectedDelete = client._types.get(typeName)?.fields.some((field) => field.name === 'version' && field.type.name === 'RecordVersion');
|
|
98
|
+
result.deleted = delta.deleted.map((item) => {
|
|
99
|
+
if (item === null) return null;
|
|
100
|
+
if (!protectedDelete) return typeof item === 'object' ? item.id : item;
|
|
101
|
+
if (typeof item !== 'object') throw new Error('Protected collection deletion requires id and version');
|
|
102
|
+
return { id: item.id, version: versionValue(item.version) };
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return result;
|
|
106
|
+
};
|
package/src/metadata.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// This module reads wire metadata only; it deliberately imports no server code.
|
|
2
|
+
const kinds = { entity: 'OBJECT', object: 'OBJECT', input: 'INPUT_OBJECT', enum: 'ENUM', scalar: 'SCALAR', interface: 'INTERFACE', union: 'UNION' };
|
|
3
|
+
const namePattern = /^[_A-Za-z][_0-9A-Za-z]*$/;
|
|
4
|
+
const record = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
5
|
+
const invalid = (detail) => { throw new Error(`Invalid client descriptor: ${detail}`); };
|
|
6
|
+
|
|
7
|
+
export const parseTypeReference = (text, types) => {
|
|
8
|
+
if (typeof text !== 'string' || text.length > 1024) invalid('invalid type reference');
|
|
9
|
+
let position = 0;
|
|
10
|
+
const read = (depth = 0) => {
|
|
11
|
+
if (depth > 64) invalid('type reference nesting exceeds 64');
|
|
12
|
+
let ref;
|
|
13
|
+
if (text[position] === '[') {
|
|
14
|
+
position++;
|
|
15
|
+
ref = { kind: 'LIST', ofType: read(depth + 1) };
|
|
16
|
+
if (text[position++] !== ']') invalid(`invalid type reference ${text}`);
|
|
17
|
+
} else {
|
|
18
|
+
const name = text.slice(position).match(/^[_A-Za-z][_0-9A-Za-z]*/)?.[0];
|
|
19
|
+
if (!name || !Object.hasOwn(types, name)) invalid(`unknown type ${name || text}`);
|
|
20
|
+
position += name.length;
|
|
21
|
+
ref = { kind: kinds[types[name].kind], name };
|
|
22
|
+
}
|
|
23
|
+
if (text[position] === '!') { position++; ref = { kind: 'NON_NULL', ofType: ref }; }
|
|
24
|
+
return ref;
|
|
25
|
+
};
|
|
26
|
+
const result = read();
|
|
27
|
+
if (position !== text.length) invalid(`invalid type reference ${text}`);
|
|
28
|
+
return result;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const normalizeDescriptor = (document) => {
|
|
32
|
+
if (!record(document) || document.formatVersion !== 1) invalid('unsupported format version (expected 1)');
|
|
33
|
+
if (!record(document.types) || !record(document.operations) || !record(document.roots)) invalid('types, roots and operations are required');
|
|
34
|
+
const descriptor = JSON.parse(JSON.stringify(document));
|
|
35
|
+
const types = descriptor.types;
|
|
36
|
+
for (const [name, type] of Object.entries(types)) {
|
|
37
|
+
if (!namePattern.test(name) || !record(type) || !Object.hasOwn(kinds, type.kind)) invalid(`invalid type ${name}`);
|
|
38
|
+
}
|
|
39
|
+
const reference = (text) => parseTypeReference(text, types);
|
|
40
|
+
const input = (arg) => {
|
|
41
|
+
if (!record(arg) || !namePattern.test(arg.name)) invalid('invalid argument or input field');
|
|
42
|
+
return { ...arg, type: reference(arg.type) };
|
|
43
|
+
};
|
|
44
|
+
const normalized = Object.entries(types).map(([name, type]) => ({
|
|
45
|
+
name, kind: kinds[type.kind], description: type.description, isOneOf: type.isOneOf,
|
|
46
|
+
baseScalar: type.baseScalar, possibleTypes: type.possibleTypes, interfaces: type.interfaces,
|
|
47
|
+
enumValues: type.values || [],
|
|
48
|
+
...(type.kind === 'input' ? { inputFields: (type.fields || []).map(input) } : { fields: (type.fields || []).map((field) => ({
|
|
49
|
+
...input(field), args: (field.args || []).map(input),
|
|
50
|
+
extensions: { relation: field.relation || null, readOnly: field.readOnly, stateMachine: field.stateMachine,
|
|
51
|
+
...(field.virtual ? { virtual: field.virtual } : {}), ...(field.queryable !== undefined ? { queryable: field.queryable } : {}),
|
|
52
|
+
...(field.rootInputUsage ? { rootInputUsage: field.rootInputUsage } : {}) },
|
|
53
|
+
})) }),
|
|
54
|
+
}));
|
|
55
|
+
for (const root of ['query', 'mutation']) {
|
|
56
|
+
if (!record(descriptor.operations[root])) invalid(`invalid ${root} operations`);
|
|
57
|
+
const entries = Object.entries(descriptor.operations[root]);
|
|
58
|
+
const rootName = descriptor.roots[root];
|
|
59
|
+
if (rootName !== null && (typeof rootName !== 'string' || !namePattern.test(rootName))) invalid(`invalid ${root} root`);
|
|
60
|
+
if (entries.length && !rootName) invalid(`missing ${root} root`);
|
|
61
|
+
const fields = entries.map(([name, operation]) => {
|
|
62
|
+
if (!namePattern.test(name) || !record(operation) || !Array.isArray(operation.args)) invalid(`invalid operation ${name}`);
|
|
63
|
+
if (operation.type !== null && (!Object.hasOwn(types, operation.type))) invalid(`unknown operation type ${operation.type}`);
|
|
64
|
+
return { name, description: operation.description, type: reference(operation.returns), args: operation.args.map(input) };
|
|
65
|
+
});
|
|
66
|
+
if (rootName) {
|
|
67
|
+
const existing = normalized.find((type) => type.name === rootName);
|
|
68
|
+
if (existing) existing.fields = fields;
|
|
69
|
+
else normalized.push({ name: rootName, kind: 'OBJECT', fields });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
for (const [name, type] of Object.entries(types)) {
|
|
73
|
+
for (const [kind, endpoint] of Object.entries(type.endpoints || {})) {
|
|
74
|
+
const root = ['get', 'list', 'aggregate'].includes(kind) ? 'query' : 'mutation';
|
|
75
|
+
if (!Object.hasOwn(descriptor.operations[root], endpoint) || descriptor.operations[root][endpoint].type !== name) invalid(`invalid endpoint ${name}.${kind}`);
|
|
76
|
+
}
|
|
77
|
+
if (type.inputs !== undefined && !record(type.inputs)) invalid(`invalid inputs for ${name}`);
|
|
78
|
+
for (const kind of ['add', 'update']) {
|
|
79
|
+
const inputName = type.inputs?.[kind];
|
|
80
|
+
if (inputName === undefined) continue;
|
|
81
|
+
if (typeof inputName !== 'string' || !Object.hasOwn(types, inputName) || types[inputName].kind !== 'input') invalid(`invalid input ${name}.${kind}: ${inputName}`);
|
|
82
|
+
const endpoint = type.endpoints?.[kind];
|
|
83
|
+
if (endpoint === undefined) continue; // Embedded/nested inputs need not have root endpoints.
|
|
84
|
+
const operation = descriptor.operations.mutation[endpoint];
|
|
85
|
+
const argument = operation?.args.find((arg) => arg.name === 'input');
|
|
86
|
+
if (!argument) invalid(`missing input operation ${name}.${kind}`);
|
|
87
|
+
let ref = reference(argument.type); while (ref.ofType) ref = ref.ofType;
|
|
88
|
+
if (ref.name !== inputName) invalid(`input ${name}.${kind} differs from its operation`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { descriptor, schema: { types: normalized, queryType: descriptor.roots.query ? { name: descriptor.roots.query } : null,
|
|
92
|
+
mutationType: descriptor.roots.mutation ? { name: descriptor.roots.mutation } : null } };
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const typeRefSelection = (depth) => depth ? `kind name ofType { ${typeRefSelection(depth - 1)} }` : 'kind name';
|
|
96
|
+
export const introspectionQuery = (extended = true) => {
|
|
97
|
+
const ref = typeRefSelection(16);
|
|
98
|
+
const argument = `name description defaultValue type { ${ref} }`;
|
|
99
|
+
return `query IntrospectionQuery { __schema { queryType { name } mutationType { name } types {
|
|
100
|
+
name kind description fields(includeDeprecated: true) { name description type { ${ref} } args { ${argument} }
|
|
101
|
+
${extended ? 'extensions { relation { displayField embedded connectionField } stateMachine readOnly }' : ''}
|
|
102
|
+
} inputFields { ${argument} } enumValues(includeDeprecated: true) { name } possibleTypes { name } interfaces { name }
|
|
103
|
+
} } }`;
|
|
104
|
+
};
|
package/src/selection.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const namePattern = /^[_A-Za-z][_0-9A-Za-z]*$/;
|
|
2
|
+
const tokensOf = (text) => {
|
|
3
|
+
const tokens = []; let index = 0;
|
|
4
|
+
const token = /(?:\s+|#[^\n\r]*|,|\.\.\.|"""[\s\S]*?"""|"(?:\\.|[^"\\])*"|[_A-Za-z][_0-9A-Za-z]*|-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?|[!$():=@[\]{}])/y;
|
|
5
|
+
while (index < text.length) {
|
|
6
|
+
token.lastIndex = index; const match = token.exec(text);
|
|
7
|
+
if (!match) throw new Error(`Invalid field selection at position ${index}`);
|
|
8
|
+
index = token.lastIndex;
|
|
9
|
+
if (!/^(?:\s|#|,)/.test(match[0])) tokens.push(match[0]);
|
|
10
|
+
}
|
|
11
|
+
return tokens;
|
|
12
|
+
};
|
|
13
|
+
const node = (name, alias = null) => ({ name, alias, suffix: '', selection: new SelectionTree() });
|
|
14
|
+
|
|
15
|
+
export class SelectionTree {
|
|
16
|
+
constructor() { this.nodes = new Map(); }
|
|
17
|
+
addScalars(fields) { for (const name of fields.trim().split(/\s+/)) { if (!name) continue; if (!namePattern.test(name)) throw new Error(`Invalid field ${name}`); if (!this.nodes.has(name)) this.nodes.set(name, node(name)); } }
|
|
18
|
+
addSelection(text) {
|
|
19
|
+
const tokens = tokensOf(text); let index = 0;
|
|
20
|
+
const name = () => { const value = tokens[index++]; if (!namePattern.test(value || '')) throw new Error('Invalid field selection name'); return value; };
|
|
21
|
+
const balanced = () => {
|
|
22
|
+
const output = []; let depth = 0;
|
|
23
|
+
do { const value = tokens[index++]; if (value === undefined) throw new Error('Unclosed selection arguments'); output.push(value); if (value === '(') depth++; if (value === ')') depth--; } while (depth);
|
|
24
|
+
return output.join(' ');
|
|
25
|
+
};
|
|
26
|
+
const read = (tree, nested = false) => {
|
|
27
|
+
while (index < tokens.length && tokens[index] !== '}') {
|
|
28
|
+
let field;
|
|
29
|
+
if (tokens[index] === '...') {
|
|
30
|
+
index++; const fragment = name();
|
|
31
|
+
field = node(fragment === 'on' ? `... on ${name()}` : `...${fragment}`);
|
|
32
|
+
} else {
|
|
33
|
+
const first = name();
|
|
34
|
+
field = tokens[index] === ':' ? (index++, node(name(), first)) : node(first);
|
|
35
|
+
}
|
|
36
|
+
if (tokens[index] === '(') field.suffix += balanced();
|
|
37
|
+
while (tokens[index] === '@') { index++; field.suffix += ` @${name()}`; if (tokens[index] === '(') field.suffix += balanced(); }
|
|
38
|
+
if (tokens[index] === '{') { index++; read(field.selection, true); }
|
|
39
|
+
const key = field.alias || field.name;
|
|
40
|
+
tree.mergeNode(key, field);
|
|
41
|
+
}
|
|
42
|
+
if (nested && tokens[index++] !== '}') throw new Error('Unclosed field selection');
|
|
43
|
+
};
|
|
44
|
+
read(this);
|
|
45
|
+
if (index !== tokens.length) throw new Error('Unexpected closing selection brace');
|
|
46
|
+
}
|
|
47
|
+
mergeNode(key, field) {
|
|
48
|
+
const previous = this.nodes.get(key);
|
|
49
|
+
if (!previous) { this.nodes.set(key, field); return; }
|
|
50
|
+
if (previous.name !== field.name || previous.suffix !== field.suffix) throw new Error(`Conflicting selections for ${key}`);
|
|
51
|
+
for (const [child, value] of field.selection.nodes) previous.selection.mergeNode(child, value);
|
|
52
|
+
}
|
|
53
|
+
addNested(path, fields) {
|
|
54
|
+
let tree = this;
|
|
55
|
+
for (const part of path.split('.')) {
|
|
56
|
+
if (!namePattern.test(part)) throw new Error(`Invalid collection path ${path}`);
|
|
57
|
+
if (!tree.nodes.has(part)) tree.nodes.set(part, node(part));
|
|
58
|
+
tree = tree.nodes.get(part).selection;
|
|
59
|
+
}
|
|
60
|
+
tree.addSelection(fields);
|
|
61
|
+
}
|
|
62
|
+
isEmpty() { return this.nodes.size === 0; }
|
|
63
|
+
serialize(context, typeName, prefix = '') {
|
|
64
|
+
const output = [];
|
|
65
|
+
for (const field of this.nodes.values()) {
|
|
66
|
+
const key = field.alias || field.name; const path = field.name.startsWith('...') ? prefix : prefix ? `${prefix}.${key}` : key;
|
|
67
|
+
const info = context?.types.get(typeName)?.fields.find((f) => f.name === field.name);
|
|
68
|
+
let suffix = field.suffix;
|
|
69
|
+
if (context?.arguments.has(path)) {
|
|
70
|
+
if (!info?.type.isList || info.type.kind !== 'OBJECT') throw new Error(`Not a collection field: ${path}`);
|
|
71
|
+
if (suffix) throw new Error(`Arguments already provided for ${path}`);
|
|
72
|
+
context.used.add(path);
|
|
73
|
+
const provided = context.arguments.get(path); const args = [];
|
|
74
|
+
for (const [name, value] of Object.entries(provided)) {
|
|
75
|
+
if (value === undefined) continue;
|
|
76
|
+
const arg = info.args.find((a) => a.name === name);
|
|
77
|
+
if (!arg) throw new Error(`Unknown collection argument ${path}.${name}`);
|
|
78
|
+
args.push(`${name}: $${context.variable(arg.type, value)}`);
|
|
79
|
+
}
|
|
80
|
+
if (args.length) suffix = `(${args.join(', ')})`;
|
|
81
|
+
}
|
|
82
|
+
const fragmentType = field.name.startsWith('... on ') ? field.name.slice(7) : typeName;
|
|
83
|
+
const children = field.selection.serialize(context, info?.type.name || fragmentType, path);
|
|
84
|
+
output.push(`${field.alias ? `${field.alias}: ` : ''}${field.name}${suffix}${children ? ` { ${children} }` : ''}`);
|
|
85
|
+
}
|
|
86
|
+
return output.join(' ');
|
|
87
|
+
}
|
|
88
|
+
}
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/** HTTP-only browser client. No server or Node ambient types are required. */
|
|
2
|
+
export interface TypeReference { readonly kind: string; readonly name?: string | null; readonly ofType?: TypeReference | null; }
|
|
3
|
+
export interface UnwrappedType { name: string; kind: string; isList: boolean; isNonNull: boolean; }
|
|
4
|
+
export interface InputValue { readonly name: string; readonly type: string; readonly description?: string | null; readonly hasDefault?: boolean; readonly defaultValue?: string; }
|
|
5
|
+
export interface DescriptorOperation { readonly operation: 'get' | 'list' | 'aggregate' | 'add' | 'update' | 'delete' | 'transition' | 'transaction' | 'customQuery' | 'customMutation'; readonly type: string | null; readonly args: readonly InputValue[]; readonly returns: string; readonly description?: string | null; readonly view?: string; readonly action?: string; readonly from?: string | null; readonly to?: string | null; }
|
|
6
|
+
export interface DescriptorField extends InputValue { readonly kind?: string; readonly readOnly?: boolean; readonly stateMachine?: boolean; readonly queryable?: boolean; readonly unique?: boolean; readonly relation?: { readonly target: string; readonly embedded: boolean; readonly connectionField: string | null; readonly displayField: string | null; readonly inverseField?: string | null }; readonly args?: readonly InputValue[]; readonly virtual?: { readonly mcp: 'tool' | 'select' | false }; readonly rootInputUsage?: Readonly<{ add?: 'required' | 'optional'; update?: 'required' | 'optional' }>; readonly deprecationReason?: string | null; }
|
|
7
|
+
export interface DescriptorStateMachine { readonly field: 'state'; readonly enum: string; readonly initialState: string | null; readonly states: readonly string[]; readonly actions: readonly { readonly name: string; readonly mutation: string; readonly description: string | null; readonly from: string | null; readonly to: string | null }[]; }
|
|
8
|
+
export interface DescriptorType { readonly kind: 'entity' | 'object' | 'interface' | 'union' | 'input' | 'enum' | 'scalar'; readonly description?: string | null; readonly stateMachine?: DescriptorStateMachine | null; readonly fields?: readonly DescriptorField[]; readonly values?: readonly { readonly name: string; readonly description?: string | null; readonly deprecationReason?: string | null }[]; readonly endpoints?: Readonly<Partial<Record<'get' | 'list' | 'aggregate' | 'add' | 'update' | 'delete', string>>>; readonly inputs?: Readonly<{ add?: string; update?: string }>; readonly baseScalar?: string | null; readonly isOneOf?: boolean; readonly possibleTypes?: readonly string[]; readonly interfaces?: readonly string[]; }
|
|
9
|
+
export interface ClientDescriptorDocument { readonly formatVersion: 1; readonly generator?: { readonly name: string; readonly version: string }; readonly roots: { readonly query: string | null; readonly mutation: string | null }; readonly types: Readonly<Record<string, DescriptorType>>; readonly operations: { readonly query: Readonly<Record<string, DescriptorOperation>>; readonly mutation: Readonly<Record<string, DescriptorOperation>> }; }
|
|
10
|
+
export interface ClientRequestInit { method?: string; headers?: Record<string, string>; body?: string; }
|
|
11
|
+
export interface ClientResponse { readonly ok: boolean; readonly status: number; json(): Promise<unknown>; }
|
|
12
|
+
export type ClientFetch = (url: string, options?: ClientRequestInit) => Promise<ClientResponse>;
|
|
13
|
+
export interface SimfinityClientOptions { prepareHeaders?: (headers: Record<string, string>) => unknown; fetch?: ClientFetch; descriptor?: ClientDescriptorDocument; descriptorEndpoint?: string; introspection?: 'extended' | 'standard'; }
|
|
14
|
+
export interface FieldExtensions { relation?: { displayField?: string | null; embedded?: boolean | null; connectionField?: string | null; target?: string; inverseField?: string | null } | null; stateMachine?: boolean | null; readOnly?: boolean | null; virtual?: { mcp: 'tool' | 'select' | false }; queryable?: boolean; rootInputUsage?: { add?: 'required' | 'optional'; update?: 'required' | 'optional' }; }
|
|
15
|
+
export interface TypeField { name: string; type: UnwrappedType; rawType: TypeReference; args: ArgumentInfo[]; extensions: FieldExtensions | null; hasDefault?: boolean; defaultValue?: unknown; }
|
|
16
|
+
export interface TypeInfo { name: string; kind: string; fields: TypeField[]; enumValues: string[]; baseScalar?: string | null; isOneOf?: boolean; possibleTypes?: readonly string[]; interfaces?: readonly string[]; }
|
|
17
|
+
export interface ArgumentInfo { name: string; type: TypeReference; hasDefault?: boolean; defaultValue?: unknown; }
|
|
18
|
+
export interface OperationInfo { name: string; args: ArgumentInfo[]; returnType: UnwrappedType; rawReturnType: TypeReference; category?: string; typeName?: string | null; view?: string; action?: string; }
|
|
19
|
+
export interface SelectionSetResult { selection: string; columns: string[]; sortFieldByColumn: Record<string, string>; fieldTypeByColumn: Record<string, string>; }
|
|
20
|
+
export interface QueryNamesForType { pluralQueryName: string | null; singularQueryName: string | null; aggregateQueryName: string | null; }
|
|
21
|
+
export interface Transition { action: string; mutationName: string; }
|
|
22
|
+
export interface FilterTerm { path?: string; operator?: string; value?: unknown; value2?: unknown; valuePath?: string | null; }
|
|
23
|
+
export interface FilterCondition extends FilterTerm { field: string; }
|
|
24
|
+
export interface QuantifiedGroup extends FilterGroup { path: string; }
|
|
25
|
+
export interface FilterGroup { conditions?: readonly FilterCondition[]; AND?: readonly FilterGroup[]; OR?: readonly FilterGroup[]; ANY?: readonly QuantifiedGroup[]; ALL?: readonly QuantifiedGroup[]; NONE?: readonly QuantifiedGroup[]; }
|
|
26
|
+
export interface FieldFilter extends FilterTerm { terms?: readonly FilterTerm[]; quantifier?: 'ANY' | 'ALL' | 'NONE'; }
|
|
27
|
+
export interface ExecWithMetaResult<T = unknown> { data: T; extensions: Record<string, unknown> | null; }
|
|
28
|
+
export interface QueryBuilder<T = Record<string, unknown>> {
|
|
29
|
+
where(field: string, operator: string, value?: unknown, value2?: unknown): this;
|
|
30
|
+
where(field: string, terms: readonly FilterTerm[]): this;
|
|
31
|
+
where(field: string, filter: FieldFilter): this;
|
|
32
|
+
filter(group: FilterGroup): this; or(groups: readonly FilterGroup[]): this; and(groups: readonly FilterGroup[]): this;
|
|
33
|
+
joinCollection(path: string, fields: string, filter?: readonly FilterTerm[]): this;
|
|
34
|
+
joinObject(path: string, fields: string): this; collectionArgs(path: string, args: Record<string, unknown>): this;
|
|
35
|
+
fields(selection: string): this; page(page: number, size: number, count?: boolean): this;
|
|
36
|
+
sort(field: string, order: 'ASC' | 'DESC'): this; clearSort(): this; autoSelect(): this;
|
|
37
|
+
exec(): Promise<(T[] & { __extensions?: Record<string, unknown> }) | null>;
|
|
38
|
+
execWithMeta(): Promise<ExecWithMetaResult<T[] | null>>;
|
|
39
|
+
}
|
|
40
|
+
export interface AggregateBuilder<T = Record<string, unknown>> {
|
|
41
|
+
groupBy(field: string): this; fact(operation: string, factName: string, path?: string): this;
|
|
42
|
+
where(field: string, operator: string, value?: unknown, value2?: unknown): this;
|
|
43
|
+
where(field: string, terms: readonly FilterTerm[]): this; where(field: string, filter: FieldFilter): this;
|
|
44
|
+
filter(group: FilterGroup): this; or(groups: readonly FilterGroup[]): this; and(groups: readonly FilterGroup[]): this;
|
|
45
|
+
page(page: number, size: number, count?: boolean): this; sort(field: string, order: 'ASC' | 'DESC'): this; exec(): Promise<T[]>;
|
|
46
|
+
}
|
|
47
|
+
export interface TransformInputOptions { mode?: 'create' | 'update'; skipFields?: readonly string[]; transientFields?: readonly string[]; preserveEmpty?: boolean; includeCollections?: boolean; inputType?: string; }
|
|
48
|
+
export interface CollectionDelta { added?: readonly Record<string, unknown>[]; updated?: readonly Record<string, unknown>[]; deleted?: readonly (string | Record<string, unknown> | null)[]; }
|
|
49
|
+
export interface CollectionDeltaOptions extends TransformInputOptions { connectionField?: string | null; }
|
|
50
|
+
export interface MutationOptions extends TransformInputOptions { transform?: boolean; }
|
|
51
|
+
export interface GraphQLErrorData { message: string; extensions?: Record<string, unknown>; path?: readonly (string | number)[]; locations?: readonly { line: number; column: number }[]; }
|
|
52
|
+
export interface GraphQLResponse<T = Record<string, unknown>> { data?: T | null; errors?: GraphQLErrorData[]; extensions?: Record<string, unknown>; }
|
|
53
|
+
export default class SimfinityClient {
|
|
54
|
+
constructor(endpoint: string, options?: SimfinityClientOptions);
|
|
55
|
+
init(): Promise<void>; getDescriptor(): ClientDescriptorDocument | null;
|
|
56
|
+
find<T = Record<string, unknown>>(typeName: string): QueryBuilder<T>;
|
|
57
|
+
findView<T = Record<string, unknown>>(name: string, params?: Record<string, unknown>): QueryBuilder<T>;
|
|
58
|
+
getViews(typeName?: string): OperationInfo[];
|
|
59
|
+
aggregate<T = Record<string, unknown>>(typeName: string): AggregateBuilder<T>;
|
|
60
|
+
query<T = unknown>(name: string, args?: Record<string, unknown>, fields?: string): Promise<T>;
|
|
61
|
+
findByParent<T = Record<string, unknown>>(typeName: string, connectionField: string, parentId: string): QueryBuilder<T>;
|
|
62
|
+
search<T = Record<string, unknown>>(typeName: string, searchTerm: string, options?: { displayField?: string | null; page?: number; size?: number }): Promise<T[] | null>;
|
|
63
|
+
getById<T = Record<string, unknown>>(typeName: string, id: string, fields?: string): Promise<T | null>;
|
|
64
|
+
add<T = Record<string, unknown>>(typeName: string, input: Record<string, unknown>, fields?: string, options?: MutationOptions): Promise<T>;
|
|
65
|
+
update<T = Record<string, unknown>>(typeName: string, id: string, input: Record<string, unknown>, fields?: string, options?: MutationOptions): Promise<T | null>;
|
|
66
|
+
delete<T = Record<string, unknown>>(typeName: string, id: string, fields?: string, options?: { version?: number }): Promise<T | null>;
|
|
67
|
+
transition<T = Record<string, unknown>>(typeName: string, action: string, id: string, inputOrFields?: Record<string, unknown> | string, fields?: string): Promise<T | null>;
|
|
68
|
+
transaction<T = Record<string, unknown>>(operations: readonly Record<string, unknown>[], fields?: string, options?: { name?: string }): Promise<T[] | null>;
|
|
69
|
+
customMutation<T = unknown>(name: string, args?: Record<string, unknown>, fields?: string): Promise<T>;
|
|
70
|
+
execute<T = Record<string, unknown>>(query: string, variables?: Record<string, unknown>): Promise<GraphQLResponse<T>>;
|
|
71
|
+
getTypes(): Record<string, TypeInfo>; getQueries(): Record<string, OperationInfo>; getMutations(): Record<string, OperationInfo>;
|
|
72
|
+
buildSelectionSet(typeName: string): SelectionSetResult;
|
|
73
|
+
getFieldExtensions(typeName: string, fieldName: string): FieldExtensions | null;
|
|
74
|
+
getDisplayField(typeName: string, fieldName: string): string | null;
|
|
75
|
+
isEmbeddedField(typeName: string, fieldName: string): boolean;
|
|
76
|
+
getConnectionField(typeName: string, fieldName: string): string | null;
|
|
77
|
+
isStateMachineField(typeName: string, fieldName: string): boolean;
|
|
78
|
+
isReadOnlyField(typeName: string, fieldName: string): boolean;
|
|
79
|
+
getEnumValues(typeName: string): string[]; getFieldsOfType(typeName: string): TypeField[];
|
|
80
|
+
getDescriptionFieldType(typeName: string, descriptionField: string): string;
|
|
81
|
+
getTypeNameForQuery(queryName: string): string | null;
|
|
82
|
+
getPluralQueryName(typeName: string): string | null; getSingularQueryName(typeName: string): string | null;
|
|
83
|
+
getListEntityNames(): string[]; getListEntityNamesOfType(typeName: string): string[];
|
|
84
|
+
getQueryNamesForType(typeName: string): QueryNamesForType;
|
|
85
|
+
getActualScalarType(scalarName: string): string | null; isNumericScalar(scalarName: string): boolean;
|
|
86
|
+
isBooleanScalar(scalarName: string): boolean; isDateTimeScalar(scalarName: string): boolean;
|
|
87
|
+
getStateMachineFields(typeName: string): string[]; getAvailableTransitions(typeName: string): Transition[];
|
|
88
|
+
transformInput(typeName: string, rawInput: Record<string, unknown>, options?: TransformInputOptions): Record<string, unknown>;
|
|
89
|
+
transformCollectionDelta(typeName: string, delta: CollectionDelta, options?: CollectionDeltaOptions): Record<string, unknown>;
|
|
90
|
+
static condition(field: string, operator: string, value?: unknown, path?: string): FilterGroup;
|
|
91
|
+
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(export GIT_EXEC_PATH=/opt/homebrew/libexec/git-core)",
|
|
5
|
-
"Bash(git --version)",
|
|
6
|
-
"Bash(/opt/homebrew/bin/git status:*)",
|
|
7
|
-
"Bash(/usr/local/bin/git status:*)",
|
|
8
|
-
"Read(//opt/homebrew/bin/**)",
|
|
9
|
-
"Bash(GIT=/opt/homebrew/bin/git)",
|
|
10
|
-
"Bash($GIT diff:*)",
|
|
11
|
-
"Bash($GIT log:*)",
|
|
12
|
-
"Bash($GIT add:*)",
|
|
13
|
-
"Bash($GIT commit:*)",
|
|
14
|
-
"Bash($GIT checkout:*)",
|
|
15
|
-
"Bash($GIT merge:*)"
|
|
16
|
-
]
|
|
17
|
-
}
|
|
18
|
-
}
|