@unispechq/unispec-core 0.2.11 → 0.2.13

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.
@@ -4,16 +4,16 @@ exports.toOpenAPI = toOpenAPI;
4
4
  exports.toGraphQLSDL = toGraphQLSDL;
5
5
  exports.toWebSocketModel = toWebSocketModel;
6
6
  function toOpenAPI(doc) {
7
- const rest = (doc.protocols?.rest ?? {});
7
+ const rest = (doc.service?.protocols?.rest ?? {});
8
8
  const info = {
9
- title: doc.name,
10
- description: doc.description,
9
+ title: doc.service.name,
10
+ description: doc.service.description,
11
11
  };
12
12
  const servers = [];
13
13
  const paths = {};
14
14
  const components = {};
15
- // Map doc.schemas into OpenAPI components.schemas
16
- const schemas = (doc.schemas ?? {});
15
+ // Map doc.service.schemas into OpenAPI components.schemas
16
+ const schemas = (doc.service?.schemas ?? {});
17
17
  const componentsSchemas = {};
18
18
  for (const [name, def] of Object.entries(schemas)) {
19
19
  componentsSchemas[name] = def.jsonSchema;
@@ -138,13 +138,16 @@ function toGraphQLSDL(doc) {
138
138
  // via a Query field. This does not attempt to interpret the full GraphQL
139
139
  // protocol structure yet, but provides a stable, deterministic SDL shape
140
140
  // based on top-level UniSpec document fields.
141
- const graphql = doc.protocols?.graphql;
141
+ const graphql = doc.service?.protocols?.graphql;
142
142
  const customSDL = graphql?.schema;
143
143
  if (typeof customSDL === "string" && customSDL.trim()) {
144
- return { sdl: customSDL };
144
+ return {
145
+ sdl: customSDL,
146
+ url: graphql?.url
147
+ };
145
148
  }
146
- const title = doc.name;
147
- const description = doc.description ?? "";
149
+ const title = doc.service.name;
150
+ const description = doc.service.description ?? "";
148
151
  const lines = [];
149
152
  if (title || description) {
150
153
  lines.push("\"\"");
@@ -165,14 +168,17 @@ function toGraphQLSDL(doc) {
165
168
  lines.push(" _serviceInfo: String!\n");
166
169
  lines.push("}");
167
170
  const sdl = lines.join("\n");
168
- return { sdl };
171
+ return {
172
+ sdl,
173
+ url: graphql?.url
174
+ };
169
175
  }
170
176
  function toWebSocketModel(doc) {
171
177
  // Base WebSocket model intended for a modern, dashboard-oriented UI.
172
178
  // It exposes service metadata, a normalized list of channels and the raw
173
179
  // websocket protocol object, while also embedding the original UniSpec
174
180
  // document under a technical key for debugging and introspection.
175
- const websocket = (doc.protocols?.websocket ?? {});
181
+ const websocket = (doc.service?.protocols?.websocket ?? {});
176
182
  const channelsArray = Array.isArray(websocket.channels) ? websocket.channels : [];
177
183
  const channels = [...channelsArray]
178
184
  .sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""))
@@ -186,10 +192,11 @@ function toWebSocketModel(doc) {
186
192
  }));
187
193
  return {
188
194
  service: {
189
- name: doc.name,
195
+ name: doc.service.name,
190
196
  title: undefined,
191
- description: doc.description,
197
+ description: doc.service.description,
192
198
  },
199
+ url: websocket.url,
193
200
  channels,
194
201
  rawProtocol: websocket,
195
202
  "x-unispec-ws": doc,
@@ -19,10 +19,10 @@ function normalizeValue(value) {
19
19
  return value;
20
20
  }
21
21
  function normalizeRestRoutes(doc) {
22
- if (!doc || !doc.protocols) {
22
+ if (!doc || !doc.service?.protocols) {
23
23
  return doc;
24
24
  }
25
- const protocols = doc.protocols;
25
+ const protocols = doc.service.protocols;
26
26
  const rest = protocols.rest;
27
27
  if (!rest || !Array.isArray(rest.routes)) {
28
28
  return doc;
@@ -37,10 +37,10 @@ function normalizeRestRoutes(doc) {
37
37
  return doc;
38
38
  }
39
39
  function normalizeWebSocket(doc) {
40
- if (!doc || !doc.protocols) {
40
+ if (!doc || !doc.service?.protocols) {
41
41
  return doc;
42
42
  }
43
- const protocols = doc.protocols;
43
+ const protocols = doc.service.protocols;
44
44
  const websocket = protocols.websocket;
45
45
  if (!websocket || !Array.isArray(websocket.channels)) {
46
46
  return doc;
@@ -68,10 +68,10 @@ function normalizeWebSocket(doc) {
68
68
  return doc;
69
69
  }
70
70
  function normalizeGraphqlOperations(doc) {
71
- if (!doc || !doc.protocols) {
71
+ if (!doc || !doc.service?.protocols) {
72
72
  return doc;
73
73
  }
74
- const protocols = doc.protocols;
74
+ const protocols = doc.service.protocols;
75
75
  const graphql = protocols.graphql;
76
76
  if (!graphql) {
77
77
  return doc;
@@ -32,21 +32,24 @@ function resolveSchemaRef(doc, ref) {
32
32
  const name = normalizeSchemaRef(ref);
33
33
  if (!name)
34
34
  return undefined;
35
- return doc.schemas?.[name];
35
+ return doc.service?.schemas?.[name];
36
36
  }
37
37
  function registerSchema(doc, name, jsonSchema) {
38
- if (!doc.schemas) {
39
- doc.schemas = {};
38
+ if (!doc.service) {
39
+ doc.service = { name: "" };
40
+ }
41
+ if (!doc.service.schemas) {
42
+ doc.service.schemas = {};
40
43
  }
41
44
  const definition = {
42
45
  jsonSchema,
43
46
  };
44
- doc.schemas[name] = definition;
47
+ doc.service.schemas[name] = definition;
45
48
  return definition;
46
49
  }
47
50
  function updateSchemaRefs(doc, mapping) {
48
- const rest = doc.protocols?.rest;
49
- const websocket = doc.protocols?.websocket;
51
+ const rest = doc.service?.protocols?.rest;
52
+ const websocket = doc.service?.protocols?.websocket;
50
53
  if (rest?.routes) {
51
54
  for (const route of rest.routes) {
52
55
  for (const params of [route.pathParams, route.queryParams, route.headers]) {
@@ -106,13 +109,13 @@ function updateSchemaRefs(doc, mapping) {
106
109
  }
107
110
  }
108
111
  function dedupeSchemas(doc) {
109
- if (!doc.schemas)
112
+ if (!doc.service?.schemas)
110
113
  return;
111
- const names = Object.keys(doc.schemas);
114
+ const names = Object.keys(doc.service.schemas);
112
115
  const hashToName = new Map();
113
116
  const renameMap = {};
114
117
  for (const name of names) {
115
- const schema = doc.schemas[name];
118
+ const schema = doc.service.schemas[name];
116
119
  const hash = stableStringify(schema?.jsonSchema ?? null);
117
120
  const existing = hashToName.get(hash);
118
121
  if (!existing) {
@@ -126,6 +129,6 @@ function dedupeSchemas(doc) {
126
129
  return;
127
130
  updateSchemaRefs(doc, renameMap);
128
131
  for (const dup of duplicates) {
129
- delete doc.schemas[dup];
132
+ delete doc.service.schemas[dup];
130
133
  }
131
134
  }
@@ -396,7 +396,7 @@ exports.GENERATED_SCHEMAS = {
396
396
  "Identifier": {
397
397
  "type": "string",
398
398
  "description": "Identifier for services, operations, channels, etc.",
399
- "pattern": "^[A-Za-z_][A-Za-z0-9_.-]*$"
399
+ "pattern": "^[A-Za-z_][A-Za-z0-9_.:-]*$"
400
400
  },
401
401
  "Description": {
402
402
  "type": "string",
@@ -436,6 +436,10 @@ exports.GENERATED_SCHEMAS = {
436
436
  }
437
437
  },
438
438
  "properties": {
439
+ "url": {
440
+ "type": "string",
441
+ "description": "Optional path or URL of the GraphQL endpoint. Recommended: relative path (e.g. '/graphql'). Absolute URLs are allowed only if the service is tightly bound to a single host. If omitted, tooling may assume a framework default (commonly '/graphql')."
442
+ },
439
443
  "schema": {
440
444
  "type": "string",
441
445
  "description": "GraphQL schema SDL as a string. Canonical source of type definitions."
@@ -788,6 +792,10 @@ exports.GENERATED_SCHEMAS = {
788
792
  }
789
793
  },
790
794
  "properties": {
795
+ "url": {
796
+ "type": "string",
797
+ "description": "Optional path or URL of the WebSocket endpoint. Recommended: relative path (e.g. '/ws' or '/socket.io' for Socket.IO). Absolute URLs are allowed only if the service is tightly bound to a single host. If omitted, tooling may assume a framework default (commonly '/ws' for WebSocket servers or '/socket.io' for Socket.IO)."
798
+ },
791
799
  "channels": {
792
800
  "type": "array",
793
801
  "description": "WebSocket channels/topics exposed by the service.",
@@ -61,15 +61,10 @@ function mapAjvErrors(errors) {
61
61
  */
62
62
  async function validateUniSpec(doc, options = {}) {
63
63
  const { validateUniSpecFn } = await getValidator(options);
64
+ // Ensure the document has required fields for validation
64
65
  const docForValidation = {
65
- unispecVersion: "0.0.0",
66
- service: {
67
- name: doc.name,
68
- description: doc.description,
69
- version: doc.version,
70
- protocols: doc.protocols,
71
- schemas: doc.schemas,
72
- },
66
+ unispecVersion: doc.unispecVersion || "1.0.0",
67
+ service: doc.service,
73
68
  extensions: doc.extensions,
74
69
  };
75
70
  const valid = validateUniSpecFn(docForValidation);
@@ -4,6 +4,7 @@ export interface OpenAPIDocument {
4
4
  }
5
5
  export interface GraphQLSDLOutput {
6
6
  sdl: string;
7
+ url?: string;
7
8
  }
8
9
  export interface WebSocketModel {
9
10
  [key: string]: unknown;
@@ -1,14 +1,14 @@
1
1
  export function toOpenAPI(doc) {
2
- const rest = (doc.protocols?.rest ?? {});
2
+ const rest = (doc.service?.protocols?.rest ?? {});
3
3
  const info = {
4
- title: doc.name,
5
- description: doc.description,
4
+ title: doc.service.name,
5
+ description: doc.service.description,
6
6
  };
7
7
  const servers = [];
8
8
  const paths = {};
9
9
  const components = {};
10
- // Map doc.schemas into OpenAPI components.schemas
11
- const schemas = (doc.schemas ?? {});
10
+ // Map doc.service.schemas into OpenAPI components.schemas
11
+ const schemas = (doc.service?.schemas ?? {});
12
12
  const componentsSchemas = {};
13
13
  for (const [name, def] of Object.entries(schemas)) {
14
14
  componentsSchemas[name] = def.jsonSchema;
@@ -133,13 +133,16 @@ export function toGraphQLSDL(doc) {
133
133
  // via a Query field. This does not attempt to interpret the full GraphQL
134
134
  // protocol structure yet, but provides a stable, deterministic SDL shape
135
135
  // based on top-level UniSpec document fields.
136
- const graphql = doc.protocols?.graphql;
136
+ const graphql = doc.service?.protocols?.graphql;
137
137
  const customSDL = graphql?.schema;
138
138
  if (typeof customSDL === "string" && customSDL.trim()) {
139
- return { sdl: customSDL };
139
+ return {
140
+ sdl: customSDL,
141
+ url: graphql?.url
142
+ };
140
143
  }
141
- const title = doc.name;
142
- const description = doc.description ?? "";
144
+ const title = doc.service.name;
145
+ const description = doc.service.description ?? "";
143
146
  const lines = [];
144
147
  if (title || description) {
145
148
  lines.push("\"\"");
@@ -160,14 +163,17 @@ export function toGraphQLSDL(doc) {
160
163
  lines.push(" _serviceInfo: String!\n");
161
164
  lines.push("}");
162
165
  const sdl = lines.join("\n");
163
- return { sdl };
166
+ return {
167
+ sdl,
168
+ url: graphql?.url
169
+ };
164
170
  }
165
171
  export function toWebSocketModel(doc) {
166
172
  // Base WebSocket model intended for a modern, dashboard-oriented UI.
167
173
  // It exposes service metadata, a normalized list of channels and the raw
168
174
  // websocket protocol object, while also embedding the original UniSpec
169
175
  // document under a technical key for debugging and introspection.
170
- const websocket = (doc.protocols?.websocket ?? {});
176
+ const websocket = (doc.service?.protocols?.websocket ?? {});
171
177
  const channelsArray = Array.isArray(websocket.channels) ? websocket.channels : [];
172
178
  const channels = [...channelsArray]
173
179
  .sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""))
@@ -181,10 +187,11 @@ export function toWebSocketModel(doc) {
181
187
  }));
182
188
  return {
183
189
  service: {
184
- name: doc.name,
190
+ name: doc.service.name,
185
191
  title: undefined,
186
- description: doc.description,
192
+ description: doc.service.description,
187
193
  },
194
+ url: websocket.url,
188
195
  channels,
189
196
  rawProtocol: websocket,
190
197
  "x-unispec-ws": doc,
@@ -16,10 +16,10 @@ function normalizeValue(value) {
16
16
  return value;
17
17
  }
18
18
  function normalizeRestRoutes(doc) {
19
- if (!doc || !doc.protocols) {
19
+ if (!doc || !doc.service?.protocols) {
20
20
  return doc;
21
21
  }
22
- const protocols = doc.protocols;
22
+ const protocols = doc.service.protocols;
23
23
  const rest = protocols.rest;
24
24
  if (!rest || !Array.isArray(rest.routes)) {
25
25
  return doc;
@@ -34,10 +34,10 @@ function normalizeRestRoutes(doc) {
34
34
  return doc;
35
35
  }
36
36
  function normalizeWebSocket(doc) {
37
- if (!doc || !doc.protocols) {
37
+ if (!doc || !doc.service?.protocols) {
38
38
  return doc;
39
39
  }
40
- const protocols = doc.protocols;
40
+ const protocols = doc.service.protocols;
41
41
  const websocket = protocols.websocket;
42
42
  if (!websocket || !Array.isArray(websocket.channels)) {
43
43
  return doc;
@@ -65,10 +65,10 @@ function normalizeWebSocket(doc) {
65
65
  return doc;
66
66
  }
67
67
  function normalizeGraphqlOperations(doc) {
68
- if (!doc || !doc.protocols) {
68
+ if (!doc || !doc.service?.protocols) {
69
69
  return doc;
70
70
  }
71
- const protocols = doc.protocols;
71
+ const protocols = doc.service.protocols;
72
72
  const graphql = protocols.graphql;
73
73
  if (!graphql) {
74
74
  return doc;
@@ -27,21 +27,24 @@ export function resolveSchemaRef(doc, ref) {
27
27
  const name = normalizeSchemaRef(ref);
28
28
  if (!name)
29
29
  return undefined;
30
- return doc.schemas?.[name];
30
+ return doc.service?.schemas?.[name];
31
31
  }
32
32
  export function registerSchema(doc, name, jsonSchema) {
33
- if (!doc.schemas) {
34
- doc.schemas = {};
33
+ if (!doc.service) {
34
+ doc.service = { name: "" };
35
+ }
36
+ if (!doc.service.schemas) {
37
+ doc.service.schemas = {};
35
38
  }
36
39
  const definition = {
37
40
  jsonSchema,
38
41
  };
39
- doc.schemas[name] = definition;
42
+ doc.service.schemas[name] = definition;
40
43
  return definition;
41
44
  }
42
45
  function updateSchemaRefs(doc, mapping) {
43
- const rest = doc.protocols?.rest;
44
- const websocket = doc.protocols?.websocket;
46
+ const rest = doc.service?.protocols?.rest;
47
+ const websocket = doc.service?.protocols?.websocket;
45
48
  if (rest?.routes) {
46
49
  for (const route of rest.routes) {
47
50
  for (const params of [route.pathParams, route.queryParams, route.headers]) {
@@ -101,13 +104,13 @@ function updateSchemaRefs(doc, mapping) {
101
104
  }
102
105
  }
103
106
  export function dedupeSchemas(doc) {
104
- if (!doc.schemas)
107
+ if (!doc.service?.schemas)
105
108
  return;
106
- const names = Object.keys(doc.schemas);
109
+ const names = Object.keys(doc.service.schemas);
107
110
  const hashToName = new Map();
108
111
  const renameMap = {};
109
112
  for (const name of names) {
110
- const schema = doc.schemas[name];
113
+ const schema = doc.service.schemas[name];
111
114
  const hash = stableStringify(schema?.jsonSchema ?? null);
112
115
  const existing = hashToName.get(hash);
113
116
  if (!existing) {
@@ -121,6 +124,6 @@ export function dedupeSchemas(doc) {
121
124
  return;
122
125
  updateSchemaRefs(doc, renameMap);
123
126
  for (const dup of duplicates) {
124
- delete doc.schemas[dup];
127
+ delete doc.service.schemas[dup];
125
128
  }
126
129
  }
@@ -5,6 +5,7 @@ export interface UniSpecGraphQLOperation {
5
5
  deprecationReason?: string;
6
6
  }
7
7
  export interface UniSpecGraphQLProtocol {
8
+ url?: string;
8
9
  schema?: string;
9
10
  queries?: UniSpecGraphQLOperation[];
10
11
  mutations?: UniSpecGraphQLOperation[];
@@ -26,6 +27,7 @@ export interface UniSpecWebSocketChannel {
26
27
  extensions?: Record<string, unknown>;
27
28
  }
28
29
  export interface UniSpecWebSocketProtocol {
30
+ url?: string;
29
31
  channels?: UniSpecWebSocketChannel[];
30
32
  securitySchemes?: Record<string, Record<string, unknown>>;
31
33
  }
@@ -86,12 +88,16 @@ export interface UniSpecServiceProtocols {
86
88
  websocket?: UniSpecWebSocketProtocol;
87
89
  }
88
90
  export interface UniSpecDocument {
91
+ unispecVersion: string;
92
+ service: UniSpecService;
93
+ extensions?: Record<string, unknown>;
94
+ }
95
+ export interface UniSpecService {
89
96
  name: string;
90
97
  description?: string;
91
98
  version?: string;
92
99
  protocols?: UniSpecServiceProtocols;
93
100
  schemas?: UniSpecSchemas;
94
- extensions?: Record<string, unknown>;
95
101
  }
96
102
  export interface ValidationError {
97
103
  message: string;
@@ -411,6 +411,10 @@ export declare const GENERATED_SCHEMAS: {
411
411
  Channel?: undefined;
412
412
  };
413
413
  properties: {
414
+ url: {
415
+ type: string;
416
+ description: string;
417
+ };
414
418
  schema: {
415
419
  type: string;
416
420
  description: string;
@@ -618,6 +622,7 @@ export declare const GENERATED_SCHEMAS: {
618
622
  additionalProperties: boolean;
619
623
  };
620
624
  };
625
+ url?: undefined;
621
626
  schema?: undefined;
622
627
  queries?: undefined;
623
628
  mutations?: undefined;
@@ -706,6 +711,7 @@ export declare const GENERATED_SCHEMAS: {
706
711
  schemas: {
707
712
  $ref: string;
708
713
  };
714
+ url?: undefined;
709
715
  schema?: undefined;
710
716
  queries?: undefined;
711
717
  mutations?: undefined;
@@ -797,6 +803,10 @@ export declare const GENERATED_SCHEMAS: {
797
803
  SchemaDefinition?: undefined;
798
804
  };
799
805
  properties: {
806
+ url: {
807
+ type: string;
808
+ description: string;
809
+ };
800
810
  channels: {
801
811
  type: string;
802
812
  description: string;
@@ -393,7 +393,7 @@ export const GENERATED_SCHEMAS = {
393
393
  "Identifier": {
394
394
  "type": "string",
395
395
  "description": "Identifier for services, operations, channels, etc.",
396
- "pattern": "^[A-Za-z_][A-Za-z0-9_.-]*$"
396
+ "pattern": "^[A-Za-z_][A-Za-z0-9_.:-]*$"
397
397
  },
398
398
  "Description": {
399
399
  "type": "string",
@@ -433,6 +433,10 @@ export const GENERATED_SCHEMAS = {
433
433
  }
434
434
  },
435
435
  "properties": {
436
+ "url": {
437
+ "type": "string",
438
+ "description": "Optional path or URL of the GraphQL endpoint. Recommended: relative path (e.g. '/graphql'). Absolute URLs are allowed only if the service is tightly bound to a single host. If omitted, tooling may assume a framework default (commonly '/graphql')."
439
+ },
436
440
  "schema": {
437
441
  "type": "string",
438
442
  "description": "GraphQL schema SDL as a string. Canonical source of type definitions."
@@ -785,6 +789,10 @@ export const GENERATED_SCHEMAS = {
785
789
  }
786
790
  },
787
791
  "properties": {
792
+ "url": {
793
+ "type": "string",
794
+ "description": "Optional path or URL of the WebSocket endpoint. Recommended: relative path (e.g. '/ws' or '/socket.io' for Socket.IO). Absolute URLs are allowed only if the service is tightly bound to a single host. If omitted, tooling may assume a framework default (commonly '/ws' for WebSocket servers or '/socket.io' for Socket.IO)."
795
+ },
788
796
  "channels": {
789
797
  "type": "array",
790
798
  "description": "WebSocket channels/topics exposed by the service.",
@@ -54,15 +54,10 @@ function mapAjvErrors(errors) {
54
54
  */
55
55
  export async function validateUniSpec(doc, options = {}) {
56
56
  const { validateUniSpecFn } = await getValidator(options);
57
+ // Ensure the document has required fields for validation
57
58
  const docForValidation = {
58
- unispecVersion: "0.0.0",
59
- service: {
60
- name: doc.name,
61
- description: doc.description,
62
- version: doc.version,
63
- protocols: doc.protocols,
64
- schemas: doc.schemas,
65
- },
59
+ unispecVersion: doc.unispecVersion || "1.0.0",
60
+ service: doc.service,
66
61
  extensions: doc.extensions,
67
62
  };
68
63
  const valid = validateUniSpecFn(docForValidation);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unispechq/unispec-core",
3
- "version": "0.2.11",
3
+ "version": "0.2.13",
4
4
  "description": "Central UniSpec Core Engine providing parsing, validation, normalization, diffing, and conversion of UniSpec specs.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -49,7 +49,7 @@
49
49
  "release:major": "node scripts/release.js major"
50
50
  },
51
51
  "dependencies": {
52
- "@unispechq/unispec-schema": "^0.3.4",
52
+ "@unispechq/unispec-schema": "^0.3.6",
53
53
  "ajv": "^8.12.0"
54
54
  },
55
55
  "optionalDependencies": {