Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@ovotech/avro-ts

Package Overview
Dependencies
Maintainers
120
Versions
33
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ovotech/avro-ts - npm Package Compare versions

Comparing version 3.1.0 to 4.0.0

dist/convert.d.ts

67

dist/index.d.ts

@@ -1,65 +0,2 @@

import { Schema, schema } from 'avsc';
import * as ts from 'typescript';
export interface Registry {
[key: string]: ts.InterfaceDeclaration;
}
declare type UnionRegistry = {
[key: string]: {
members: string[];
namespace: string;
};
};
export interface Context {
recordAlias: string;
namesAlias: string;
namespacedPrefix: string;
registry: Registry;
unionRegistry: UnionRegistry;
recordNeedsNS: boolean;
parentName?: string;
namespace?: string;
namespaces: {
[key: string]: ts.TypeReferenceNode;
};
logicalTypes: {
[key: string]: ts.TypeReferenceNode;
};
visitedLogicalTypes: Array<string>;
}
export interface Result<TsType = ts.TypeNode> {
type: TsType;
context: Context;
}
export declare type Convert<TType = Schema> = (context: Context, type: TType) => Result<any>;
export declare const result: <TsType = ts.TypeNode>(context: Context, type: TsType) => Result<TsType>;
export declare const mapContext: <T = any, TsType = ts.TypeNode>(context: Context, items: T[], callbackfn: (context: Context, item: T) => Result<TsType>) => {
items: TsType[];
context: Context;
};
export declare const withContexts: (context: Context, items: Context[]) => Context;
export declare const withEntry: (context: Context, entry: ts.InterfaceDeclaration) => Context;
export declare const withNamespace: (context: Context, record: schema.RecordType) => Context;
export interface State {
output: string[];
repository: {
[key: string]: string;
};
logicalTypes: {
[key: string]: string;
};
}
declare type LogicalTypeWithImport = {
import: string;
type: string;
};
declare type LogicalTypeDefinition = string | LogicalTypeWithImport;
declare type AvroTsOptions = {
logicalTypes?: {
[key: string]: LogicalTypeDefinition;
};
recordAlias?: string;
namespacedPrefix?: string;
namesAlias?: string;
};
export declare function avroTs(recordType: schema.RecordType, options?: AvroTsOptions): string;
export {};
export { toTypeScript, convertType } from './convert';
export { Context, CustomLogicalType } from './types';
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const ts = require("typescript");
exports.result = (context, type) => ({
context,
type,
});
exports.mapContext = (context, items, callbackfn) => items.reduce((all, item) => {
const current = callbackfn(all.context, item);
return {
items: [...all.items, current.type],
context: current.context,
};
}, { items: [], context });
exports.withContexts = (context, items) => items.reduce((all, itemContext) => ({
...all,
registry: { ...all.registry, ...itemContext.registry },
namespaces: { ...all.namespaces, ...itemContext.namespaces },
}), context);
exports.withEntry = (context, entry) => ({
...context,
registry: { ...context.registry, [entry.name.text]: entry },
});
exports.withNamespace = (context, record) => ({
...context,
namespace: record.namespace,
namespaces: {
...context.namespaces,
[fullyQualifiedName(context, record)]: ts.createTypeReferenceNode(record.name, undefined),
},
});
const docToJSDoc = (doc) => `*\n${doc
.split('\n')
.map(line => ` * ${line}`)
.join('\n')}\n `;
const convertRecord = (context, type) => {
const namespaceContext = type.namespace ? exports.withNamespace(context, type) : context;
const fieldContext = { ...namespaceContext, recordNeedsNS: false };
const fields = type.fields.map(fieldType => {
const field = convertType({ ...fieldContext, parentName: type.name }, fieldType.type);
const prop = ts.createPropertySignature(undefined, fieldType.name, isOptional(fieldType.type) ? ts.createToken(ts.SyntaxKind.QuestionToken) : undefined, field.type, undefined);
const propWithDoc = fieldType.doc
? ts.addSyntheticLeadingComment(prop, ts.SyntaxKind.MultiLineCommentTrivia, docToJSDoc(fieldType.doc), true)
: prop;
return exports.result(field.context, propWithDoc);
});
const interfaceType = ts.createInterfaceDeclaration(undefined, [ts.createToken(ts.SyntaxKind.ExportKeyword)], type.name, undefined, undefined, fields.map(field => field.type));
const recordContext = exports.withContexts(exports.withEntry(fieldContext, interfaceType), fields.map(item => item.context));
if (context.recordNeedsNS) {
const namespaced = fullyQualifiedName(context, type);
const props = [
ts.createPropertySignature(undefined, ts.createStringLiteral(namespaced), undefined, ts.createTypeReferenceNode(type.name, undefined), undefined),
];
const registryKey = getUnionRegistryKey(type, context);
if (registryKey) {
const registryEntry = context.unionRegistry[registryKey] || {};
props.push(...(registryEntry.members || [])
.filter((name) => name !== type.name)
.map(name => ts.createPropertySignature(undefined, ts.createStringLiteral(`${registryEntry.namespace}.${name}`), ts.createToken(ts.SyntaxKind.QuestionToken), ts.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword), undefined)));
}
const namespacedInterfaceType = ts.createInterfaceDeclaration(undefined, [ts.createToken(ts.SyntaxKind.ExportKeyword)], `${context.namespacedPrefix}${type.name}`, undefined, undefined, props);
return exports.result(exports.withEntry(recordContext, namespacedInterfaceType), ts.createTypeReferenceNode(namespacedInterfaceType.name.text, undefined));
}
return exports.result(recordContext, ts.createTypeReferenceNode(type.name, undefined));
};
const convertType = (context, type) => {
if (typeof type === 'string') {
return convertPredefinedType(context, type);
}
else if (Array.isArray(type)) {
return convertArrayType(context, type);
}
else if (isLogicalType(type)) {
return convertLogicalType(context, type);
}
else if (isRecordType(type)) {
return convertRecord(context, type);
}
else if (isArrayType(type)) {
const itemType = convertType(context, type.items);
return exports.result(itemType.context, ts.createArrayTypeNode(itemType.type));
}
else if (isMapType(type)) {
return convertMapType(context, type);
}
else if (isEnumType(type)) {
return convertEnum(context, type);
}
else {
throw new Error(`Cannot work out type ${type}`);
}
};
const convertPrimitive = (context, avroType) => {
switch (avroType) {
case 'long':
case 'int':
case 'double':
case 'float':
return exports.result(context, ts.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword));
case 'bytes':
return exports.result(context, ts.createTypeReferenceNode('Buffer', undefined));
case 'null':
return exports.result(context, ts.createNull());
case 'boolean':
return exports.result(context, ts.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword));
case 'string':
return exports.result(context, ts.createKeywordTypeNode(ts.SyntaxKind.StringKeyword));
default:
return exports.result(context, ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword));
}
};
const convertEnum = (context, enumType) => exports.result(context, ts.createUnionTypeNode(enumType.symbols.map(symbol => ts.createLiteralTypeNode(ts.createLiteral(symbol)))));
const convertLogicalType = (context, type) => {
if (context.logicalTypes[type.logicalType]) {
if (!context.visitedLogicalTypes.includes(type.logicalType))
context.visitedLogicalTypes.push(type.logicalType);
return exports.result(context, context.logicalTypes[type.logicalType]);
}
return convertPrimitive(context, type.type);
};
const convertPredefinedType = (context, type) => context.namespaces[type] ? exports.result(context, context.namespaces[type]) : convertPrimitive(context, type);
const recordNeedsNS = (items) => {
return items.filter(item => typeof item === 'object' && !Array.isArray(item) && isRecordType(item)).length > 1;
};
const convertArrayType = (context, type) => {
const map = exports.mapContext(context, type, (itemContext, item) => {
if (typeof item === 'object' && !Array.isArray(item) && isRecordType(item)) {
return convertType({ ...itemContext, recordNeedsNS: recordNeedsNS(type) }, item);
}
else {
return convertType(itemContext, item);
}
});
return exports.result(map.context, ts.createUnionTypeNode(map.items));
};
const convertMapType = (context, type) => {
const map = convertType(context, type.values);
return exports.result(map.context, ts.createTypeLiteralNode([
ts.createIndexSignature(undefined, undefined, [
ts.createParameter(undefined, undefined, undefined, 'index', undefined, ts.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), undefined),
], map.type),
]));
};
const isRecordType = (type) => typeof type === 'object' && 'type' in type && type.type === 'record';
const isArrayType = (type) => typeof type === 'object' && 'type' in type && type.type === 'array';
const isMapType = (type) => typeof type === 'object' && 'type' in type && type.type === 'map';
const isEnumType = (type) => typeof type === 'object' && 'type' in type && type.type === 'enum';
const isLogicalType = (type) => typeof type === 'object' && 'logicalType' in type;
const isUnion = (type) => typeof type === 'object' && Array.isArray(type);
const isRecordParent = (type) => typeof type === 'object' && typeof type.type === 'object';
const isUnionParent = (type) => Array.isArray(type.type);
const isOptional = (type) => {
if (isUnion(type)) {
const t1 = type[0];
if (typeof t1 === 'string') {
return t1 === 'null';
}
}
return false;
};
const fullyQualifiedName = (context, type) => {
const currentNamespace = type.namespace || context.namespace;
return currentNamespace ? `${currentNamespace}.${type.name}` : type.name;
};
const printAstNode = (nodes, { importLines }) => {
const resultFile = ts.createSourceFile('someFileName.ts', '', ts.ScriptTarget.Latest);
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
return importLines.concat(nodes.map(n => printer.printNode(ts.EmitHint.Unspecified, n, resultFile))).join('\n\n');
};
const defaultOptions = {
recordAlias: 'Record',
namespacedPrefix: 'Namespaced',
namesAlias: 'Names',
};
function avroTs(recordType, options = {}) {
const logicalTypes = options.logicalTypes || {};
const isRootUnion = Array.isArray(recordType) && recordNeedsNS(recordType);
const context = {
...options,
recordAlias: options.recordAlias || defaultOptions.recordAlias,
namesAlias: options.namesAlias || defaultOptions.namesAlias,
namespacedPrefix: options.namespacedPrefix || defaultOptions.namespacedPrefix,
recordNeedsNS: isRootUnion,
registry: {},
unionRegistry: buildUnionRegistry({}, recordType, { namespace: recordType.namespace, recordNeedsNS: isRootUnion }),
namespaces: {},
visitedLogicalTypes: [],
logicalTypes: Object.entries(logicalTypes).reduce((all, [name, type]) => {
const typeStr = type.type ? type.type : type;
return {
...all,
[name]: ts.createTypeReferenceNode(typeStr, undefined),
};
}, {}),
};
const mainNode = convertType(context, recordType);
const importLines = context.visitedLogicalTypes
.map(visitedType => logicalTypes[visitedType].import)
.filter(Boolean);
const nodes = [
ts.createTypeAliasDeclaration(undefined, [ts.createToken(ts.SyntaxKind.ExportKeyword)], context.recordAlias, undefined, mainNode.type),
].concat(Object.values(mainNode.context.registry));
const namesNamespace = unionRegisryToNamespace(context.unionRegistry, context.namesAlias);
if (namesNamespace) {
nodes.unshift(namesNamespace);
}
return printAstNode(nodes, { importLines });
}
exports.avroTs = avroTs;
function unionRegisryToNamespace(registry, namespaceName) {
const names = Object.keys(registry).reduce((nodes, key) => {
const { namespace, members } = registry[key];
return nodes.concat(members.map(name => ts.createVariableStatement([ts.createToken(ts.SyntaxKind.ExportKeyword)], ts.createVariableDeclarationList([ts.createVariableDeclaration(name, undefined, ts.createLiteral(`${namespace}.${name}`))], ts.NodeFlags.Const))));
}, []);
if (!names.length) {
return;
}
const nsNode = ts.createModuleDeclaration([], [ts.createToken(ts.SyntaxKind.ExportKeyword)], ts.createIdentifier(namespaceName), ts.createModuleBlock(names), ts.NodeFlags.Namespace);
return nsNode;
}
const getUnionRegistryKey = (type, { parentName, namespace }) => {
const currentNamespace = type.namespace || namespace;
if (currentNamespace) {
return parentName ? `${currentNamespace}.${parentName}` : currentNamespace;
}
};
function buildUnionRegistry(registry, schema, context) {
if (Array.isArray(schema)) {
return schema.reduce((acc, schema) => buildUnionRegistry(acc, schema, context), registry);
}
if (isRecordParent(schema)) {
return buildUnionRegistry(registry, schema.type, {
...context,
recordNeedsNS: isUnionParent(schema) && recordNeedsNS(schema.type),
});
}
if (isRecordType(schema)) {
const { name, fields } = schema;
const namespace = schema.namespace || context.namespace;
const registryKey = getUnionRegistryKey(schema, context);
if (registryKey && context.recordNeedsNS) {
if (!registry[registryKey]) {
registry[registryKey] = { namespace: namespace, members: [] };
}
registry[registryKey].members.push(name);
}
fields.reduce((acc, field) =>
// @ts-ignore This works, but a bit too dynamic for TS
buildUnionRegistry(acc, field, {
...context,
unionMember: false,
parentName: schema.name,
namespace: schema.namespace || context.namespace,
}), registry);
}
return registry;
}
//# sourceMappingURL=index.js.map
var convert_1 = require("./convert");
exports.toTypeScript = convert_1.toTypeScript;
exports.convertType = convert_1.convertType;
{
"name": "@ovotech/avro-ts",
"description": "Convert avro schemas into typescript interfaces",
"version": "3.1.0",
"version": "4.0.0",
"main": "dist/index.js",

@@ -11,28 +11,31 @@ "source": "src/index.ts",

"scripts": {
"test-js": "jest --runInBand",
"test-ts": "tsc test/integration.ts --strict --noEmit && ! tsc test/integration-should-fail.ts --strict --noEmit",
"test": "yarn test-js && yarn test-ts",
"lint-prettier": "prettier --list-different {src,test}/**/*.ts",
"lint-tslint": "tslint --config tslint.json '{src,test}/**/*.ts'",
"lint": "yarn lint-prettier && yarn lint-tslint",
"build": "tsc --outDir dist --declaration"
"test:js": "jest --runInBand",
"test:ts": "tsc test/integration.ts --strict --noEmit && ! tsc test/integration-should-fail.ts --strict --noEmit",
"test": "yarn test:js && yarn test:ts",
"lint:prettier": "prettier --list-different {src,test}/**/*.ts",
"lint:eslint": "eslint '{src,test}/**/*.ts'",
"lint": "yarn lint:prettier && yarn lint:eslint",
"build": "tsc --outDir dist --declaration",
"build:docs": "build-docs README.md"
},
"devDependencies": {
"@ovotech/avro-logical-types": "^1.0.0",
"@types/jest": "^24.0.13",
"@types/node": "^11.11.4",
"avsc": "^5.4.10",
"jest": "^24.8.0",
"prettier": "^1.17.1",
"ts-jest": "^24.0.2",
"tslint": "^5.17.0",
"tslint-config-prettier": "^1.18.0"
"@types/jest": "^24.0.24",
"@types/node": "^13.1.0",
"avsc": "^5.4.18",
"eslint-config-prettier": "^6.7.0",
"jest": "^24.9.0",
"moment": "^2.24.0",
"prettier": "^1.19.1",
"ts-jest": "^24.3.0",
"ts-node": "^8.5.4"
},
"jest": {
"preset": "../../jest-preset.json"
"preset": "../../jest.json"
},
"dependencies": {
"typescript": "^3.5.1"
"@ovotech/ts-compose": "^0.10.0",
"typescript": "^3.7.4"
},
"gitHead": "25d55039d4369fb71b8573486d2942e365f85ba5"
"gitHead": "821a55c53b17a4055675132e7a272710a3dc6a5a"
}

@@ -5,3 +5,3 @@ # Avro TS

It consists of a very quick sequential, functional parser. Uses typescript's compiler api to convert avro to typescript AST, and pretty prints the results. No dependencies apart from typescript.
Uses typescript's compiler api to convert avro to typescript AST, and pretty prints the results.

@@ -16,19 +16,60 @@ ### Using

> [examples/simple.ts](examples/simple.ts)
```typescript
import { schema } from 'avsc';
import { avroTs } from '@ovotech/avro-ts';
import { toTypeScript } from '@ovotech/avro-ts';
import { Schema } from 'avsc';
const avro: schema.RecordType = JSON.parse(String(readFileSync(join(__dirname, 'avro', file))));
const ts = avroTs(avro, {
const avro: Schema = {
type: 'record',
name: 'User',
fields: [
{ name: 'id', type: 'int' },
{ name: 'username', type: 'string' },
],
};
const ts = toTypeScript(avro);
console.log(ts);
```
Resulting TypeScript:
```typescript
export type AvroType = User;
export interface User {
id: number;
username: string;
}
```
### Logical Types
Avro has [logical types](https://github.com/mtth/avsc/wiki/Advanced-usage#logical-types). In their docs:
> The built-in types provided by Avro are sufficient for many use-cases, but it can often be much more convenient to work with native JavaScript objects.
To support them we need to modify the typescript generation to use the typescript type instead of the logical type. If we don't avro-ts will fall back on the original underlying type.
> [examples/logical-types.ts](examples/logical-types.ts)
```typescript
import { toTypeScript } from '@ovotech/avro-ts';
import { Schema } from 'avsc';
const avro: Schema = {
type: 'record',
name: 'Event',
fields: [
{ name: 'id', type: 'int' },
{ name: 'createdAt', type: { type: 'long', logicalType: 'timestamp-millis' } },
],
};
const ts = toTypeScript(avro, {
logicalTypes: {
'timestamp-millis': 'string',
date: 'string',
decimal: {
type: 'Decimal',
import: "import { Decimal } from 'decimal.js'",
},
},
recordAlias: 'Record',
namesAlias: 'Names',
namespacedPrefix: 'Namespaced',
});

@@ -39,29 +80,153 @@

## Support
Resulting TypeScript:
This converter currently supports
```typescript
export type AvroType = Event;
- Record
- Union
- Map
- Logical Types
- Enum
- Map
- Array
- Root-level union types
export interface Event {
id: number;
createdAt: string;
}
```
## Union types helpers.
We can also use custom classes for our logical types. It will also add the code to import the module.
When complex union types are defined, the output will include a namespace (named `Names` by default), containing the namespaced address of properties.
> [examples/custom-logical-types.ts](examples/custom-logical-types.ts)
This allows usecases as such:
```typescript
import { toTypeScript } from '@ovotech/avro-ts';
import { Schema } from 'avsc';
const avro: Schema = {
type: 'record',
name: 'Event',
fields: [
{ name: 'id', type: 'int' },
{ name: 'decimalValue', type: { type: 'long', logicalType: 'decimal' } },
{ name: 'anotherDecimal', type: { type: 'long', logicalType: 'decimal' } },
],
};
const ts = toTypeScript(avro, {
logicalTypes: {
decimal: { module: 'decimal.js', named: 'Decimal' },
},
});
console.log(ts);
```
Resulting TypeScript:
```typescript
import { Names, WeatherEvent } from './my-type';
const event: WeatherEvent = {};
event[Names.RainEvent];
import { Decimal } from 'decimal.js';
export type AvroType = Event;
export interface Event {
id: number;
decimalValue: Decimal;
anotherDecimal: Decimal;
}
```
The union members uses [`never`](https://www.typescriptlang.org/docs/handbook/basic-types.html#never) to prevent erroneously creating/accessing a union member with mutiple keys.
## Wrapped Unions
Avro Ts attempts to generate the types of the "auto" setting for wrapped unions. https://github.com/mtth/avsc/wiki/API#typeforschemaschema-opts This would mean that unions of records would be wrapped in an object with namespaced keys.
The typescript interfaces are also namespaced appropriately. Avro namespaces like 'com.example.avro' are converted into `ComExampleAvro` namespaces in TS.
> [examples/wrapped-union.ts](examples/wrapped-union.ts)
```typescript
import { toTypeScript } from '@ovotech/avro-ts';
import { Schema } from 'avsc';
const avro: Schema = {
type: 'record',
name: 'Event',
namespace: 'com.example.avro',
fields: [
{ name: 'id', type: 'int' },
{
name: 'event',
type: [
{
type: 'record',
name: 'ElectricityEvent',
fields: [
{ name: 'accountId', type: 'string' },
{ name: 'MPAN', type: 'string' },
],
},
{
type: 'record',
name: 'GasEvent',
fields: [
{ name: 'accountId', type: 'string' },
{ name: 'MPRN', type: 'string' },
],
},
],
},
],
};
const ts = toTypeScript(avro);
console.log(ts);
```
Which would result in this typescript:
```typescript
/* eslint-disable @typescript-eslint/no-namespace */
export type Event = ComExampleAvro.Event;
export namespace ComExampleAvro {
export const ElectricityEventName = 'com.example.avro.ElectricityEvent';
export interface ElectricityEvent {
accountId: string;
MPAN: string;
}
export const GasEventName = 'com.example.avro.GasEvent';
export interface GasEvent {
accountId: string;
MPRN: string;
}
export const EventName = 'com.example.avro.Event';
export interface Event {
id: number;
event:
| {
'com.example.avro.ElectricityEvent': ComExampleAvro.ElectricityEvent;
'com.example.avro.GasEvent'?: never;
}
| {
'com.example.avro.ElectricityEvent'?: never;
'com.example.avro.GasEvent': ComExampleAvro.GasEvent;
};
}
}
```
Notice that not only the interfaces themselves are exported, but their fully qualified names as well. This should help to improve readability.
We also breakout the root type from its namespace for ease of use.
```typescript
import { ComExampleAvro as NS, Event } from '...';
const elecEvent: Event = {
id: 10,
event: { [NS.ElectricityEventName]: { MPAN: '111', accountId: '123' } },
};
const gasEvent: Event = {
id: 10,
event: { [NS.GasEventName]: { MPRN: '222', accountId: '123' } },
};
```
## Running the tests

@@ -68,0 +233,0 @@

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc