├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── cli.ts ├── components │ ├── Avro │ │ ├── AvroRecord.ts │ │ ├── BaseAvroRecord.ts │ │ ├── KafkaAvroMessage.ts │ │ ├── compression │ │ │ ├── DataCompression.ts │ │ │ ├── NullCompression.ts │ │ │ ├── SnappyAdapter.ts │ │ │ └── ZlibAdapter.ts │ │ ├── schema_registry │ │ │ ├── CachedSchemaRegistryClient.ts │ │ │ └── SchemaRegistry.ts │ │ └── serde │ │ │ ├── BaseKafkaAvroSerde.ts │ │ │ ├── KafkaAvroDeserializer.ts │ │ │ ├── KafkaAvroFactory.ts │ │ │ ├── KafkaAvroSerializer.ts │ │ │ └── Serializable.ts │ ├── Compiler │ │ ├── Compiler.ts │ │ └── base │ │ │ └── BaseCompiler.ts │ └── Converters │ │ ├── ClassConverter.ts │ │ ├── EnumConverter.ts │ │ ├── LogicalTypeConverter.ts │ │ ├── PrimitiveConverter.ts │ │ ├── RecordConverter.ts │ │ └── base │ │ └── BaseConverter.ts ├── helpers │ ├── ConsoleHelper.ts │ ├── DirHelper.ts │ ├── FileHelper.ts │ ├── SpecialCharacterHelper.ts │ └── TypeHelper.ts ├── index.ts ├── interfaces │ ├── AvroSchema.ts │ └── CompilerOutput.ts ├── models │ └── ExportModel.ts └── templates │ ├── typescriptClassMethodsTemplate │ └── typescriptClassTemplate ├── test ├── components │ ├── Compiler.ts │ └── Converters │ │ ├── ClassConverter.ts │ │ ├── EnumConverter.ts │ │ ├── PrimitiveConverter.ts │ │ └── RecordConverter.ts └── data │ ├── avro │ ├── ComplexRecord.avsc │ ├── RecordWithEnum.avsc │ ├── RecordWithInterface.avsc │ ├── RecordWithLogicalTypes.avsc │ ├── RecordWithMap.avsc │ ├── RecordWithUnion.avsc │ ├── SimpleEnum.avsc │ ├── SimpleRecord.avsc │ ├── SimpleRecord.json │ ├── TradeCollection.avsc │ └── User.avsc │ └── expected │ ├── BaseAvroRecord.ts.test │ ├── ComplexRecord.ts.test │ ├── RecordWithEnum.ts.test │ ├── RecordWithInterface.ts.test │ ├── RecordWithLogicalTypes.ts.test │ ├── RecordWithLogicalTypesMapped.ts.test │ ├── RecordWithMap.ts.test │ ├── RecordWithUnion.ts.test │ ├── SimpleEnum.ts.test │ ├── SimpleRecord.ts.test │ ├── TradeCollection.ts.test │ └── User.ts.test ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | .idea/ 3 | dist/ 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Degordian 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | avro-to-typescript 2 | ======== 3 | [![Build Status](https://travis-ci.org/degordian/avro-to-typescript.svg?branch=master)](https://travis-ci.org/degordian/avro-to-typescript) 4 | [![npm version](https://badge.fury.io/js/%40degordian%2Favro-to-typescript.svg)](https://badge.fury.io/js/%40degordian%2Favro-to-typescript) 5 | 6 | avro-to-typescript compiles avro schema files (.avsc) to TypeScript classes 7 | and interfaces. Making using avro schematics with node.js easy and comfortable. 8 | 9 | 10 | Features 11 | -------- 12 | 13 | - Compiles most if not all avro types (**record**, **enum**, **primitive**, **map**, **array**) 14 | - Provides methods for effective serialization and deserialization of avro to js and vice versa 15 | 16 | 17 | Usage 18 | ----- 19 | 20 | #### Global: 21 | Most projects will use avro-to-typescript this way 22 | ```sh 23 | npm install -g @degordian/avro-to-typescript 24 | 25 | avro-to-typescript --compile [ schema-directory ] [ output-directory ] 26 | ``` 27 | This will generate namespaced folders and files for your schemas inside 28 | output directory so have that in mind. 29 | 30 | You also need to install avro-to-typescript in your project. 31 | ``` 32 | npm install @degordian/avro-to-typescript --save 33 | ``` 34 | 35 | #### Project: 36 | This way is if your projects needs to generate avro classes while running. 37 | ``` 38 | npm install @degordian/avro-to-typescript --save 39 | ``` 40 | 41 | import { Compiler } from "degordian/avro-to-typescript"; 42 | 43 | const compiler = new Compiler(outputDir); 44 | await compiler.compile(avro); 45 | 46 | #### Logical Types: 47 | If you want to take advantage of logical types, you can pass these as an argument to the cli 48 | ```sh 49 | avro-to-typescript --compile [ schema-directory ] [ output-directory ] --logical-types [avro type] [typescript type] 50 | ``` 51 | 52 | You can even pass more than one if you alternate them: 53 | ```sh 54 | avro-to-typescript --compile [ schema-directory ] [ output-directory ] --logical-types [avro type] [typescript type] [avro type] [typescript type] 55 | ``` 56 | 57 | You can also pass them to the compilre in your code directly: 58 | 59 | import { Compiler } from "degordian/avro-to-typescript"; 60 | 61 | const compiler = new Compiler(outputDir, { date: 'string', 'timestamp-millis': 'string'} }); 62 | await compiler.compile(avro); 63 | 64 | If there are logical types in the avro schema but you don't provide a mapping for them, they will default to the underlying primitive type. 65 | 66 | Contribution and Support 67 | ------------------------ 68 | 69 | If you are having issues, please let us know on our issue tracker. 70 | 71 | - Issue Tracker: github.com/degordian/avro-to-typescript/issues 72 | - Source Code: github.com/degordian/avro-to-typescript 73 | 74 | 75 | License 76 | ------- 77 | 78 | The project is licensed under the MIT license. 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@degordian/avro-to-typescript", 3 | "version": "2.2.3", 4 | "description": "Package for converting Avro schema files (.avsc) to TypeScript class files", 5 | "keywords": [ 6 | "avro", 7 | "avro schemas", 8 | "typescript", 9 | "compiler", 10 | "converter" 11 | ], 12 | "main": "dist/src/index.js", 13 | "types": "dist/src/index.d.ts", 14 | "bin": { 15 | "avro-to-typescript": "./dist/src/cli.js" 16 | }, 17 | "directories": { 18 | "doc": "docs", 19 | "dist": "dist", 20 | "src": "src", 21 | "test": "test" 22 | }, 23 | "scripts": { 24 | "start": "npm run build && node ./dist/src/index.js", 25 | "build": "tsc", 26 | "prepublishOnly": "npm run test && npm run generate-barrels", 27 | "generate-barrels": "barrelsby --directory src/ --delete --include components src/interfaces --exclude base Avro/compression Avro/schema_registry", 28 | "test": "npm run tslint & npm run mocha", 29 | "tslint": "tslint --project tsconfig.json --format stylish", 30 | "tslint-jenkins": "tslint --project tsconfig.json -o tslint.xml -t checkstyle", 31 | "mocha": "mocha dist/test --reporter=nyan --recursive", 32 | "travis:test": "npm run build && npm run test", 33 | "premocha": "npm run build" 34 | }, 35 | "author": "Degordian (https://www.degordian.com/)", 36 | "license": "MIT", 37 | "bugs": { 38 | "url": "https://github.com/degordian/avro-to-typescript/issues" 39 | }, 40 | "homepage": "https://github.com/degordian/avro-to-typescript#readme", 41 | "dependencies": { 42 | "@types/args": "^3.0.0", 43 | "@types/command-line-args": "^5.0.0", 44 | "@types/command-line-usage": "^5.0.1", 45 | "@types/fs-extra": "^5.0.3", 46 | "@types/node": "^14.0.22", 47 | "@types/schema-registry": "^1.17.0", 48 | "command-line-args": "^5.0.2", 49 | "command-line-usage": "^6.1.0", 50 | "fs-extra": "^5.0.0", 51 | "schema-registry": "^1.17.0", 52 | "typescript-memoize": "^1.0.0-alpha.3" 53 | }, 54 | "devDependencies": { 55 | "@degordian/avro-to-typescript": "latest", 56 | "@degordian/standards": "^1.0.8", 57 | "@degordian/testing-toolkit": "^1.0.18", 58 | "mocha-typescript": "^1.1.16", 59 | "tslint": "^6.1.2", 60 | "typescript": "^2.9.1" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as args from "command-line-args"; 4 | import * as cmdusage from "command-line-usage"; 5 | import * as fs from "fs"; 6 | import * as path from "path"; 7 | import { Compiler } from "./components/Compiler/Compiler"; 8 | import { ConsoleHelper } from "./helpers/ConsoleHelper"; 9 | 10 | const cmdOptions = [ 11 | { 12 | name: "compile", 13 | alias: "c", 14 | type: String, 15 | typeLabel: "{underline schema-directory} {underline output-directory}", 16 | description: "Compile schema directory into output directory", 17 | multiple: true, 18 | }, 19 | { 20 | name: "help", 21 | alias: "h", 22 | description: "Print this usage guide.", 23 | }, 24 | { 25 | name: "logical-types", 26 | type: String, 27 | typeLabel: "{underline logical-type} {underline typescript-type}", 28 | description: "Use logical types", 29 | multiple: true, 30 | }, 31 | ]; 32 | 33 | const usageOptions = [ 34 | { 35 | header: "avro-to-typescript", 36 | content: "Compile avro schemas to typescript classes with ease. It will output to set directory " + 37 | "and append namespace to path.", 38 | }, 39 | { 40 | header: "Options", 41 | optionList: cmdOptions, 42 | }, 43 | { 44 | content: "Project home: {underline https://github.com/degordian/avro-to-typescript}", 45 | }, 46 | ]; 47 | 48 | let options; 49 | let usage; 50 | 51 | try { 52 | options = args(cmdOptions); 53 | 54 | console.log(options); 55 | usage = cmdusage(usageOptions); 56 | } catch (e) { 57 | ConsoleHelper.break("Invalid value or option used"); 58 | } 59 | 60 | if (options === undefined) { 61 | throw new Error(); 62 | } 63 | 64 | if (options.compile) { 65 | let schemaDir = options.compile[0]; 66 | let classDir = options.compile[1]; 67 | 68 | if (schemaDir === undefined || classDir === undefined) { 69 | ConsoleHelper.break("Undefined"); 70 | } 71 | 72 | classDir = path.resolve(classDir); 73 | schemaDir = path.resolve(schemaDir); 74 | 75 | if (!fs.existsSync(schemaDir) || !fs.existsSync(classDir)) { 76 | ConsoleHelper.break("The directory does not exist or is invalid"); 77 | } 78 | 79 | const logicalTypes = {}; 80 | const logicalTypesMap = options["logical-types"]; 81 | if (logicalTypesMap && logicalTypesMap.length) { 82 | for (let index = 0; index < logicalTypesMap.length; index += 2) { 83 | if (!logicalTypesMap[index + 1]) { 84 | ConsoleHelper.break("Invalid logical-types, you must alternate logical type with typescript type"); 85 | } 86 | logicalTypes[logicalTypesMap[index]] = logicalTypesMap[index + 1]; 87 | } 88 | } 89 | 90 | const compiler: Compiler = new Compiler(classDir, logicalTypes); 91 | compiler.compileFolder(schemaDir); 92 | } 93 | 94 | if (options.help !== undefined) { 95 | console.log(usage); 96 | } 97 | -------------------------------------------------------------------------------- /src/components/Avro/AvroRecord.ts: -------------------------------------------------------------------------------- 1 | import { Serializable } from "./serde/Serializable"; 2 | 3 | export interface AvroRecord extends Serializable { 4 | schema(): object; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/Avro/BaseAvroRecord.ts: -------------------------------------------------------------------------------- 1 | import * as avro from "avsc"; 2 | import { Schema, Type } from "avsc"; 3 | import { Memoize } from "typescript-memoize"; 4 | import { AvroRecord } from "./AvroRecord"; 5 | 6 | export abstract class BaseAvroRecord implements AvroRecord { 7 | 8 | public static readonly subject: string = ""; 9 | public static readonly schema = {}; 10 | 11 | @Memoize((schema: any) => { 12 | return schema.namespace + schema.name; 13 | }) 14 | public static getTypeForSchema(schema: any): Type { 15 | return avro.Type.forSchema(schema); 16 | } 17 | 18 | @Memoize((schema: any) => { 19 | return schema.namespace + schema.name; 20 | }) 21 | public static createTypeResolver(baseType: Type, newType: Type): Type { 22 | return baseType.createResolver(newType) as Type; 23 | } 24 | 25 | protected static internalDeserialize(buffer: Buffer, newSchema?: object) { 26 | const baseType = BaseAvroRecord.getTypeForSchema(this.schema); 27 | let resolver: object; 28 | let noCheck = false; 29 | 30 | if (newSchema !== undefined) { 31 | const newType = BaseAvroRecord.getTypeForSchema(newSchema); 32 | resolver = BaseAvroRecord.createTypeResolver(baseType, newType); 33 | noCheck = true; 34 | } 35 | // @ts-ignore 36 | return baseType.fromBuffer(buffer, resolver, noCheck); 37 | } 38 | 39 | public loadValuesFromType(type: Type) { 40 | this.loadObjectValues(this, type, this.transformation()); 41 | } 42 | 43 | public abstract schema(): any; 44 | 45 | public abstract subject(): string; 46 | 47 | public serialize(): Buffer { 48 | const type = BaseAvroRecord.getTypeForSchema(this.schema()); 49 | return type.toBuffer(this); 50 | } 51 | 52 | /// virtual 53 | protected transformation(): object { 54 | return {}; 55 | } 56 | 57 | private loadObjectValues(result: BaseAvroRecord, object: Type, transformation: object = {}) { 58 | Object.keys(object).forEach((key) => { 59 | if (transformation.hasOwnProperty(key) && object[key] !== null) { 60 | if (Array.isArray(object[key])) { 61 | result[key] = object[key].map(transformation[key]); 62 | } else { 63 | result[key] = transformation[key](object[key]); 64 | } 65 | } else { 66 | result[key] = object[key]; 67 | } 68 | }); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/components/Avro/KafkaAvroMessage.ts: -------------------------------------------------------------------------------- 1 | export class KafkaAvroMessage { 2 | 3 | public static MAGIC_BYTE = 0; 4 | 5 | public static fromBuffer(buffer: Buffer) { 6 | const schemaId = buffer.readInt32BE(1); 7 | const avroBuffer = buffer.slice(5); 8 | return new KafkaAvroMessage(schemaId, avroBuffer); 9 | } 10 | 11 | private readonly _magicByte: number; 12 | private readonly _schemaId: number; 13 | private readonly _avroBuffer: Buffer; 14 | 15 | public constructor(schemaId: number, avroBuffer: Buffer) { 16 | this._magicByte = KafkaAvroMessage.MAGIC_BYTE; 17 | this._schemaId = schemaId; 18 | this._avroBuffer = avroBuffer; 19 | } 20 | 21 | public get magicByte(): number { 22 | return this._magicByte; 23 | } 24 | 25 | public get schemaId(): number { 26 | return this._schemaId; 27 | } 28 | 29 | public get avroBuffer(): Buffer { 30 | return this._avroBuffer; 31 | } 32 | 33 | public toBuffer(): Buffer { 34 | const preBuffer = new Buffer(5); 35 | preBuffer[0] = this.magicByte; 36 | preBuffer.writeInt32BE(this.schemaId, 1); 37 | 38 | return Buffer.concat([ 39 | preBuffer, 40 | this.avroBuffer, 41 | ]); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/components/Avro/compression/DataCompression.ts: -------------------------------------------------------------------------------- 1 | export interface DataCompression { 2 | compress(buffer: T): K; 3 | 4 | decompress(buffer: K): T; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/Avro/compression/NullCompression.ts: -------------------------------------------------------------------------------- 1 | import { DataCompression } from "./DataCompression"; 2 | 3 | export class NullCompression implements DataCompression { 4 | 5 | public compress(buffer: Buffer): Buffer { 6 | return buffer; 7 | } 8 | 9 | public decompress(buffer: Buffer): Buffer { 10 | return buffer; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/components/Avro/compression/SnappyAdapter.ts: -------------------------------------------------------------------------------- 1 | import { DataCompression } from "./DataCompression"; 2 | 3 | export class SnappyAdapter implements DataCompression { 4 | 5 | private static snappy = require("snappy"); // sadly, no types for this lib 6 | 7 | public compress(buffer: Buffer): Buffer { 8 | return SnappyAdapter.snappy.compressSync(buffer); 9 | } 10 | 11 | public decompress(buffer: Buffer): Buffer { 12 | return SnappyAdapter.snappy.uncompressSync(buffer); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/components/Avro/compression/ZlibAdapter.ts: -------------------------------------------------------------------------------- 1 | import * as zlib from "zlib"; 2 | import { ZlibOptions } from "zlib"; 3 | import { DataCompression } from "./DataCompression"; 4 | 5 | export class ZlibAdapter implements DataCompression { 6 | 7 | private _options?: ZlibOptions; 8 | private _defaultOptions: ZlibOptions = { 9 | level: 5, 10 | }; 11 | 12 | public constructor(options?: ZlibOptions) { 13 | this._options = (options) ? options : this._defaultOptions; 14 | } 15 | 16 | public compress(buffer: Buffer): Buffer { 17 | return zlib.gzipSync(buffer, this._options); 18 | } 19 | 20 | public decompress(buffer: Buffer): Buffer { 21 | return zlib.unzipSync(buffer, this._options); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/Avro/schema_registry/CachedSchemaRegistryClient.ts: -------------------------------------------------------------------------------- 1 | import { RegistryClient, RegistryClientConfig, RegistryRequest } from "schema-registry"; 2 | import { Memoize } from "typescript-memoize"; 3 | import { RecordType } from "../../.."; 4 | import { SchemaRegistry } from "./SchemaRegistry"; 5 | 6 | export class CachedSchemaRegistryClient implements SchemaRegistry { 7 | private _defaultLogger: object = { 8 | info: (): void => { 9 | return; 10 | }, 11 | log: () => { 12 | return; 13 | }, 14 | error: () => { 15 | return; 16 | }, 17 | }; 18 | private client: RegistryClient; 19 | 20 | constructor(config: RegistryClientConfig) { 21 | const clientConfig = { 22 | type: "avro", 23 | host: config.host, 24 | port: config.port, 25 | logger: (config.logger !== undefined) ? config.logger : this._defaultLogger, 26 | }; 27 | this.client = new RegistryClient(clientConfig); 28 | } 29 | 30 | @Memoize() 31 | public async getSchemaById(id: number): RegistryRequest { 32 | return await this.client.getSchemaById(id); 33 | } 34 | 35 | @Memoize() 36 | public async registerSubjectVersion(subject: string, schema: RecordType): RegistryRequest { 37 | return await this.client.registerSubjectVersion(subject, schema); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/Avro/schema_registry/SchemaRegistry.ts: -------------------------------------------------------------------------------- 1 | import { RegistryRequest } from "schema-registry"; 2 | import { RecordType } from "../../.."; 3 | 4 | export interface SchemaRegistry { 5 | getSchemaById(id: number): RegistryRequest; 6 | 7 | registerSubjectVersion(subject: string, schema: RecordType): RegistryRequest; 8 | } 9 | -------------------------------------------------------------------------------- /src/components/Avro/serde/BaseKafkaAvroSerde.ts: -------------------------------------------------------------------------------- 1 | import { RegistryClientConfig } from "schema-registry"; 2 | import { DataCompression } from "../compression/DataCompression"; 3 | import { NullCompression } from "../compression/NullCompression"; 4 | import { ZlibAdapter } from "../compression/ZlibAdapter"; 5 | import { KafkaAvroMessage } from "../KafkaAvroMessage"; 6 | import { CachedSchemaRegistryClient } from "../schema_registry/CachedSchemaRegistryClient"; 7 | import { SchemaRegistry } from "../schema_registry/SchemaRegistry"; 8 | 9 | export class BaseKafkaAvroSerde { 10 | public schemaRegistryClient: SchemaRegistry; 11 | 12 | private _dataCompression: DataCompression = new ZlibAdapter(); 13 | 14 | constructor(registryClientConfig: RegistryClientConfig) { 15 | this.schemaRegistryClient = new CachedSchemaRegistryClient(registryClientConfig); 16 | } 17 | 18 | public get dataCompression() { 19 | return this._dataCompression; 20 | } 21 | 22 | public set dataCompression(value: DataCompression) { 23 | this._dataCompression = value; 24 | } 25 | 26 | public disableCompression() { 27 | this._dataCompression = new NullCompression(); 28 | } 29 | 30 | public toKafkaAvroMessage(buffer: Buffer): KafkaAvroMessage { 31 | return KafkaAvroMessage.fromBuffer(this.dataCompression.decompress(buffer)); 32 | } 33 | 34 | public toBuffer(kafkaAvroMessage: KafkaAvroMessage): Buffer { 35 | return this.dataCompression.compress(kafkaAvroMessage.toBuffer()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/components/Avro/serde/KafkaAvroDeserializer.ts: -------------------------------------------------------------------------------- 1 | import { AvroSchemaResponseInterface } from "schema-registry"; 2 | import { BaseAvroRecord } from "../BaseAvroRecord"; 3 | import { KafkaAvroMessage } from "../KafkaAvroMessage"; 4 | import { BaseKafkaAvroSerde } from "./BaseKafkaAvroSerde"; 5 | 6 | export class KafkaAvroDeserializer extends BaseKafkaAvroSerde { 7 | 8 | private knownSchemas: Map = new Map(); 9 | 10 | public async deserialize(buffer: Buffer, deserType: { new(): T; }): Promise { 11 | const kafkaAvroMessage: KafkaAvroMessage = this.toKafkaAvroMessage(buffer); 12 | const schemaId = kafkaAvroMessage.schemaId; 13 | 14 | let newSchema = this.knownSchemas.get(schemaId); 15 | 16 | if (newSchema === undefined) { 17 | const newSchemaRaw: AvroSchemaResponseInterface = await this.schemaRegistryClient.getSchemaById(schemaId); 18 | newSchema = JSON.parse(newSchemaRaw.schema) as object; 19 | this.knownSchemas.set(schemaId, newSchema); 20 | } 21 | 22 | const avroBuffer = kafkaAvroMessage.avroBuffer; 23 | // @ts-ignore 24 | return deserType.deserialize(avroBuffer, newSchema) as T; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/components/Avro/serde/KafkaAvroFactory.ts: -------------------------------------------------------------------------------- 1 | import { RegistryClientConfig } from "schema-registry"; 2 | import { KafkaAvroDeserializer } from "./KafkaAvroDeserializer"; 3 | import { KafkaAvroSerializer } from "./KafkaAvroSerializer"; 4 | 5 | export class KafkaAvroFactory { 6 | public avroRegistryConfiguration: RegistryClientConfig; 7 | 8 | constructor(avroRegistryConfiguration: RegistryClientConfig) { 9 | this.avroRegistryConfiguration = avroRegistryConfiguration; 10 | } 11 | 12 | public createSerializer(): KafkaAvroSerializer { 13 | return new KafkaAvroSerializer(this.avroRegistryConfiguration); 14 | } 15 | 16 | public createDeserializer(): KafkaAvroDeserializer { 17 | return new KafkaAvroDeserializer(this.avroRegistryConfiguration); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/Avro/serde/KafkaAvroSerializer.ts: -------------------------------------------------------------------------------- 1 | import { AvroSchemaResponseInterface } from "schema-registry"; 2 | import { BaseAvroRecord } from "../BaseAvroRecord"; 3 | import { KafkaAvroMessage } from "../KafkaAvroMessage"; 4 | import { BaseKafkaAvroSerde } from "./BaseKafkaAvroSerde"; 5 | 6 | export class KafkaAvroSerializer extends BaseKafkaAvroSerde { 7 | 8 | public async serialize(obj: BaseAvroRecord): Promise { 9 | const schema: AvroSchemaResponseInterface = 10 | await this.schemaRegistryClient.registerSubjectVersion(obj.subject(), obj.schema()); 11 | const schemaId: number = schema.id; 12 | const buffer: Buffer = obj.serialize(); 13 | 14 | const kafkaAvroMessage: KafkaAvroMessage = new KafkaAvroMessage(schemaId, buffer); 15 | return this.toBuffer(kafkaAvroMessage); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/Avro/serde/Serializable.ts: -------------------------------------------------------------------------------- 1 | export interface Serializable { 2 | serialize(): Buffer; 3 | } 4 | -------------------------------------------------------------------------------- /src/components/Compiler/Compiler.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import { DirHelper } from "../../helpers/DirHelper"; 4 | import { TypeHelper } from "../../helpers/TypeHelper"; 5 | import { CompilerOutput } from "../../interfaces/CompilerOutput"; 6 | import { ExportModel } from "../../models/ExportModel"; 7 | import { ClassConverter } from "../Converters/ClassConverter"; 8 | import { BaseCompiler } from "./base/BaseCompiler"; 9 | 10 | export class Compiler extends BaseCompiler { 11 | public exports: ExportModel[]; 12 | 13 | public constructor(outputDir: string, public logicalTypes?: { [key: string]: string }) { 14 | super(); 15 | 16 | this.classPath = path.resolve(outputDir); 17 | } 18 | 19 | public async compileFolder(schemaPath: string): Promise { 20 | try { 21 | fs.readdir(schemaPath, async (err, files) => { 22 | for (const file of files) { 23 | const fullPath = schemaPath + path.sep + file; 24 | 25 | if (fs.statSync(fullPath).isDirectory()) { 26 | await this.compileFolder(fullPath); 27 | continue; 28 | } 29 | 30 | const data = fs.readFileSync(fullPath).toString(); 31 | 32 | await this.compile(data); 33 | } 34 | }); 35 | } catch (err) { 36 | console.log(err); 37 | } 38 | } 39 | 40 | public async compile(data: any): Promise { 41 | const classConverter = new ClassConverter(this.logicalTypes); 42 | data = classConverter.getData(data); 43 | 44 | const namespace = data.namespace.replace(/\./g, path.sep); 45 | const outputDir = `${this.classPath}${path.sep}${namespace}`; 46 | 47 | if (TypeHelper.isRecordType(data)) { 48 | classConverter.convert(data); 49 | } 50 | 51 | const result = classConverter.joinExports(); 52 | 53 | DirHelper.mkdirIfNotExist(outputDir); 54 | this.saveBaseAvroRecord(); 55 | this.saveEnums(classConverter.enumExports, outputDir); 56 | this.saveClass(outputDir, data, result); 57 | console.log(`Wrote ${data.name}.ts in ${outputDir}`); 58 | 59 | return { 60 | class: data.name, 61 | dir: outputDir, 62 | }; 63 | } 64 | 65 | protected saveClass(outputDir: string, data: any, result: string) { 66 | const classFile = `${outputDir}${path.sep}${data.name}.ts`; 67 | fs.writeFileSync(classFile, result); 68 | } 69 | 70 | protected saveEnums(enums: ExportModel[], outputDir: string) { 71 | for (const enumFile of enums) { 72 | const savePath = `${outputDir}${path.sep}${enumFile.name}Enum.ts`; 73 | 74 | fs.writeFileSync(savePath, enumFile.content); 75 | } 76 | } 77 | 78 | protected saveBaseAvroRecord() { 79 | const avroRecordPath = `${this.classPath}${path.sep}BaseAvroRecord.ts`; 80 | 81 | if (!fs.existsSync(avroRecordPath)) { 82 | fs.writeFileSync( 83 | avroRecordPath, 84 | "export { BaseAvroRecord } from \"@degordian/avro-to-typescript\";\n", 85 | ); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/components/Compiler/base/BaseCompiler.ts: -------------------------------------------------------------------------------- 1 | export abstract class BaseCompiler { 2 | private _schemaPath: string; 3 | private _classPath: string; 4 | 5 | get classPath(): string { 6 | return this._classPath; 7 | } 8 | 9 | set classPath(value: string) { 10 | this._classPath = value; 11 | } 12 | 13 | get schemaPath(): string { 14 | return this._schemaPath; 15 | } 16 | 17 | set schemaPath(value: string) { 18 | this._schemaPath = value; 19 | } 20 | 21 | public abstract compile(data: string): Promise; 22 | } 23 | -------------------------------------------------------------------------------- /src/components/Converters/ClassConverter.ts: -------------------------------------------------------------------------------- 1 | import { SpecialCharacterHelper } from "../../helpers/SpecialCharacterHelper"; 2 | import { TypeHelper } from "../../helpers/TypeHelper"; 3 | import { RecordType } from "../../interfaces/AvroSchema"; 4 | import { ExportModel } from "../../models/ExportModel"; 5 | import { RecordConverter } from "./RecordConverter"; 6 | 7 | export class ClassConverter extends RecordConverter { 8 | 9 | protected interfaceRows: string[] = []; 10 | protected interfaceSuffix = "Interface"; 11 | protected TAB = SpecialCharacterHelper.TAB; 12 | 13 | protected classRows: string[] = []; 14 | protected importRows: string[] = []; 15 | 16 | public convert(data: any): any { 17 | data = this.getData(data) as RecordType; 18 | 19 | this.classRows.push(...this.extractClass(data)); 20 | this.importRows.push(...this.extractImports(data)); 21 | 22 | this.getExportModels(data); 23 | 24 | return; 25 | } 26 | 27 | protected getExportModels(data: RecordType): ExportModel { 28 | const importExportModel: ExportModel = new ExportModel(); 29 | const classExportModel: ExportModel = new ExportModel(); 30 | const interfaceExportModel: ExportModel = new ExportModel(); 31 | 32 | importExportModel.name = "imports"; 33 | importExportModel.content = this.importRows.join(SpecialCharacterHelper.NEW_LINE); 34 | 35 | classExportModel.name = data.name; 36 | classExportModel.content = this.classRows.join(SpecialCharacterHelper.NEW_LINE); 37 | 38 | interfaceExportModel.name = data.name + this.interfaceSuffix; 39 | interfaceExportModel.content = this.interfaceRows.join(SpecialCharacterHelper.NEW_LINE); 40 | 41 | this.exports = [ 42 | importExportModel, 43 | interfaceExportModel, 44 | classExportModel, 45 | ]; 46 | 47 | return classExportModel; 48 | } 49 | 50 | protected extractImports(data: RecordType): string[] { 51 | const rows: string[] = []; 52 | const dirsUp: number = data.namespace.split(".").length; 53 | 54 | rows.push(`// tslint:disable`); 55 | rows.push(`import { BaseAvroRecord } from "` + "../".repeat(dirsUp) + `BaseAvroRecord";`); 56 | 57 | for (const enumFile of this.enumExports) { 58 | const importLine = `import { ${enumFile.name} } from "./${enumFile.name}Enum";`; 59 | rows.push(importLine); 60 | } 61 | 62 | return rows; 63 | } 64 | 65 | protected extractClass(data: RecordType): string[] { 66 | const rows: string[] = []; 67 | const interfaceRows: string[] = []; 68 | const TAB = SpecialCharacterHelper.TAB; 69 | 70 | interfaceRows.push(`export interface ${data.name}${this.interfaceSuffix} {`); 71 | rows.push(`export class ${data.name} extends BaseAvroRecord implements ${data.name}${this.interfaceSuffix} {`); 72 | rows.push(``); 73 | 74 | rows.push(`${TAB}public static readonly subject: string = "${data.name}";`); 75 | rows.push(`${TAB}public static readonly schema: object = ${JSON.stringify(data, null, 4)}`); 76 | rows.push(``); 77 | 78 | rows.push(`${TAB}public static deserialize(buffer: Buffer, newSchema?: object): ${data.name} {`); 79 | rows.push(`${TAB}${TAB}const result = new ${data.name}();`); 80 | rows.push(`${TAB}${TAB}const rawResult = this.internalDeserialize(buffer, newSchema);`); 81 | rows.push(`${TAB}${TAB}result.loadValuesFromType(rawResult);`); 82 | rows.push(``); 83 | rows.push(`${TAB}${TAB}return result;`); 84 | rows.push(`${TAB}}`); 85 | rows.push(``); 86 | 87 | for (const field of data.fields) { 88 | let fieldType; 89 | let classRow; 90 | if (TypeHelper.hasDefault(field) || TypeHelper.isOptional(field.type)) { 91 | const defaultValue = TypeHelper.hasDefault(field) ? ` = ${TypeHelper.getDefault(field)}` : ""; 92 | fieldType = `${this.getField(field)}`; 93 | classRow = `${TAB}public ${fieldType}${defaultValue};`; 94 | } else { 95 | const convertedType = this.convertType(field.type); 96 | fieldType = `${field.name}: ${convertedType}`; 97 | classRow = `${TAB}public ${field.name}!: ${convertedType};`; 98 | } 99 | 100 | interfaceRows.push(`${this.TAB}${fieldType};`); 101 | 102 | rows.push(classRow); 103 | } 104 | interfaceRows.push("}"); 105 | 106 | rows.push(``); 107 | 108 | rows.push(`${TAB}public schema(): object {`); 109 | rows.push(`${TAB}${TAB}return ${data.name}.schema;`); 110 | rows.push(`${TAB}}`); 111 | 112 | rows.push(``); 113 | 114 | rows.push(`${TAB}public subject(): string {`); 115 | rows.push(`${TAB}${TAB}return ${data.name}.subject;`); 116 | rows.push(`${TAB}}`); 117 | 118 | rows.push(`}`); 119 | 120 | this.interfaceRows.push(...interfaceRows); 121 | return rows; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/components/Converters/EnumConverter.ts: -------------------------------------------------------------------------------- 1 | import { SpecialCharacterHelper } from "../../helpers/SpecialCharacterHelper"; 2 | import { EnumType } from "../../interfaces/AvroSchema"; 3 | import { ExportModel } from "../../models/ExportModel"; 4 | import { BaseConverter } from "./base/BaseConverter"; 5 | 6 | export class EnumConverter extends BaseConverter { 7 | 8 | protected rows: string[] = []; 9 | 10 | public convert(data: any): ExportModel { 11 | data = this.getData(data) as EnumType; 12 | 13 | this.rows.push(...this.extractEnum(data)); 14 | 15 | const exportModel = new ExportModel(); 16 | exportModel.name = data.name; 17 | exportModel.content = this.rows.join(SpecialCharacterHelper.NEW_LINE); 18 | this.exports.push(exportModel); 19 | 20 | return exportModel; 21 | } 22 | 23 | protected extractEnum(data: EnumType): string[] { 24 | const rows: string[] = []; 25 | 26 | rows.push(`export enum ${data.name} {`); 27 | for (const symbol of data.symbols) { 28 | rows.push(`${SpecialCharacterHelper.TAB}${symbol},`); 29 | } 30 | rows.push(`}`); 31 | 32 | return rows; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/components/Converters/LogicalTypeConverter.ts: -------------------------------------------------------------------------------- 1 | import { LogicalType } from "../../interfaces/AvroSchema"; 2 | import { BaseConverter } from "./base/BaseConverter"; 3 | import { PrimitiveConverter } from "./PrimitiveConverter"; 4 | 5 | export class LogicalTypeConverter extends BaseConverter { 6 | public convert(data: any): string { 7 | data = this.getData(data) as LogicalType; 8 | const primitiveConverter = new PrimitiveConverter(); 9 | 10 | return this.logicalTypesMap[data.logicalType] || primitiveConverter.convert(data.type); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/components/Converters/PrimitiveConverter.ts: -------------------------------------------------------------------------------- 1 | import { BaseConverter } from "./base/BaseConverter"; 2 | 3 | export class PrimitiveConverter extends BaseConverter { 4 | 5 | public convert(type: string): string { 6 | switch (type) { 7 | case "long": 8 | case "int": 9 | case "double": 10 | case "float": 11 | return "number"; 12 | case "string": 13 | return "string"; 14 | case "bytes": 15 | return "Buffer"; 16 | case "null": 17 | return "null"; 18 | case "boolean": 19 | return "boolean"; 20 | default: 21 | return "any"; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/components/Converters/RecordConverter.ts: -------------------------------------------------------------------------------- 1 | import { SpecialCharacterHelper } from "../../helpers/SpecialCharacterHelper"; 2 | import { TypeHelper } from "../../helpers/TypeHelper"; 3 | import { Field, RecordType, Type } from "../../interfaces/AvroSchema"; 4 | import { ExportModel } from "../../models/ExportModel"; 5 | import { BaseConverter } from "./base/BaseConverter"; 6 | import { EnumConverter } from "./EnumConverter"; 7 | import { LogicalTypeConverter } from "./LogicalTypeConverter"; 8 | import { PrimitiveConverter } from "./PrimitiveConverter"; 9 | 10 | export class RecordConverter extends BaseConverter { 11 | 12 | protected interfaceRows: string[] = []; 13 | 14 | public convert(data: any): ExportModel { 15 | data = this.getData(data) as RecordType; 16 | 17 | this.interfaceRows.push(...this.extractInterface(data)); 18 | 19 | const exportModel = new ExportModel(); 20 | exportModel.name = data.name; 21 | exportModel.content = this.interfaceRows.join(SpecialCharacterHelper.NEW_LINE); 22 | this.exports.push(exportModel); 23 | 24 | return exportModel; 25 | } 26 | 27 | protected extractInterface(data: RecordType): string[] { 28 | const rows: string[] = []; 29 | 30 | rows.push(`export interface ${data.name} {`); 31 | 32 | for (const field of data.fields) { 33 | const fieldType = `${this.getField(field)};`; 34 | rows.push(`${SpecialCharacterHelper.TAB}${fieldType}`); 35 | } 36 | 37 | rows.push(`}`); 38 | 39 | return rows; 40 | } 41 | 42 | protected convertType(type: Type): string { 43 | if (typeof type === "string") { 44 | const converter = new PrimitiveConverter(); 45 | 46 | return converter.convert(type); 47 | } 48 | 49 | if (TypeHelper.isLogicalType(type)) { 50 | const converter = new LogicalTypeConverter(this.logicalTypesMap); 51 | 52 | return converter.convert(type); 53 | } 54 | 55 | if (TypeHelper.isEnumType(type)) { 56 | const converter = new EnumConverter(); 57 | const exportModel = converter.convert(type); 58 | this.enumExports.push(exportModel); 59 | 60 | return exportModel.name; 61 | } 62 | 63 | if (type instanceof Array) { 64 | return type.map((t) => this.convertType(t)).join(" | "); 65 | } 66 | 67 | if (TypeHelper.isRecordType(type)) { 68 | this.interfaceRows.push(...this.extractInterface(type)); 69 | this.interfaceRows.push(""); 70 | 71 | return type.name; 72 | } 73 | 74 | if (TypeHelper.isArrayType(type)) { 75 | return `${this.convertType(type.items)}[]`; 76 | } 77 | 78 | if (TypeHelper.isMapType(type)) { 79 | // Dictionary of types, string as key 80 | return `{ [index: string]: ${this.convertType(type.values)} }`; 81 | } 82 | 83 | this.addError(BaseConverter.errorMessages.TYPE_NOT_FOUND); 84 | return "any"; 85 | } 86 | 87 | protected getField(field: Field): string { 88 | return `${field.name}${TypeHelper.isOptional(field.type) ? "?" : ""}: ${this.convertType(field.type)}`; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/components/Converters/base/BaseConverter.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import { SpecialCharacterHelper } from "../../../helpers/SpecialCharacterHelper"; 3 | import { AvroSchema } from "../../../interfaces/AvroSchema"; 4 | import { ExportModel } from "../../../models/ExportModel"; 5 | 6 | export abstract class BaseConverter { 7 | public static errorMessages = { 8 | TYPE_NOT_FOUND: "Type not found!", 9 | }; 10 | 11 | public errorType: string; 12 | public addError: (errorMessage: string) => void; 13 | public hasErrors: () => boolean; 14 | public errors: string[]; 15 | public exports: ExportModel[] = []; 16 | public enumExports: ExportModel[] = []; 17 | public interfaceExports: ExportModel[] = []; 18 | 19 | constructor(public logicalTypesMap: { [key: string]: string } = {}) {} 20 | 21 | public abstract convert(data: any): any; 22 | 23 | public joinExports(): string { 24 | let result = this.exports 25 | .reduce((joinedExport: string, nextExport: ExportModel) => { 26 | 27 | const exports: string[] = []; 28 | 29 | if (joinedExport.length > 0) { 30 | exports.push(joinedExport); 31 | } 32 | 33 | exports.push(nextExport.content); 34 | 35 | return `${exports.join(`${SpecialCharacterHelper.NEW_LINE}${SpecialCharacterHelper.NEW_LINE}`)}`; 36 | }, ""); 37 | result += `${SpecialCharacterHelper.NEW_LINE}`; 38 | 39 | return result; 40 | } 41 | 42 | /** 43 | * This takes care of converting data to format I can use. Since you 44 | * can pass either file path, a stringified JSON or actual object. 45 | * 46 | * @param data - can be file path, JSON string or parsed json 47 | * @returns {any} 48 | */ 49 | public getData(data: any): AvroSchema { 50 | if (typeof data === "string") { 51 | try { 52 | return JSON.parse(data); 53 | } catch { 54 | return JSON.parse(fs.readFileSync(data).toString()); 55 | } 56 | } 57 | 58 | return data; 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/helpers/ConsoleHelper.ts: -------------------------------------------------------------------------------- 1 | export class ConsoleHelper { 2 | 3 | public static info(msg: string) { 4 | console.info(msg); 5 | } 6 | 7 | public static getArgs(): string[] { 8 | const args: string[] = process.argv; 9 | args.splice(0, 2); 10 | 11 | return args; 12 | } 13 | 14 | public static getUsage(): string { 15 | return this._usage; 16 | } 17 | 18 | public static break(error: string): void { 19 | console.error(error); 20 | console.info(this.getUsage()); 21 | process.exit(); 22 | } 23 | 24 | protected static _usage = "Check --help"; 25 | } 26 | -------------------------------------------------------------------------------- /src/helpers/DirHelper.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | 4 | export class DirHelper { 5 | public static mkdirIfNotExist(dir: string): string | void { 6 | if (this.exists(dir)) { 7 | return; 8 | } 9 | 10 | console.log("Directory doesn't exist, creating it..."); 11 | return dir 12 | .split(path.sep) 13 | .reduce((currentPath, folder) => { 14 | currentPath += folder + path.sep; 15 | if (!fs.existsSync(currentPath)) { 16 | fs.mkdirSync(currentPath); 17 | } 18 | return currentPath; 19 | }, ""); 20 | } 21 | 22 | public static exists(dir: string): boolean { 23 | return fs.existsSync(dir); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/helpers/FileHelper.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import ErrnoException = NodeJS.ErrnoException; 3 | import * as path from "path"; 4 | 5 | export class FileHelper { 6 | public filePath: string; 7 | 8 | constructor(filePath: string) { 9 | this.filePath = filePath; 10 | } 11 | 12 | public getContent(): Promise { 13 | return new Promise((resolve, reject) => { 14 | if (this.exists() === false) { 15 | reject(`File not found: ${this.filePath}`); 16 | return; 17 | } 18 | 19 | fs.readFile(this.filePath, {}, (error: ErrnoException, content: string | Buffer) => { 20 | if (error) { 21 | console.log(error, "Err", this.filePath); 22 | reject(error); 23 | return; 24 | } 25 | 26 | resolve(content); 27 | }); 28 | }); 29 | } 30 | 31 | public save(content: string): Promise { 32 | return new Promise((resolve, reject) => { 33 | if (this.exists() === false) { 34 | reject(`File not found: ${this.filePath}`); 35 | return; 36 | } 37 | 38 | fs.writeFileSync(this.filePath, content); 39 | resolve(true); 40 | }); 41 | } 42 | 43 | public exists(): boolean { 44 | return fs.existsSync(this.filePath); 45 | } 46 | 47 | public async create(): Promise { 48 | if (this.exists() === true) { 49 | return; 50 | } 51 | 52 | fs.writeFileSync(this.filePath, ""); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/helpers/SpecialCharacterHelper.ts: -------------------------------------------------------------------------------- 1 | export class SpecialCharacterHelper { 2 | public static TAB = ` `; 3 | public static NEW_LINE = `\n`; 4 | } 5 | -------------------------------------------------------------------------------- /src/helpers/TypeHelper.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArrayType, 3 | EnumType, 4 | Field, 5 | LogicalType, 6 | MapType, 7 | NamedType, 8 | RecordType, 9 | Type, 10 | } from "../interfaces/AvroSchema"; 11 | 12 | export class TypeHelper { 13 | 14 | public static isRecordType(schema: Type): schema is RecordType { 15 | if (typeof schema === "string" || schema instanceof Array) { 16 | return false; 17 | } 18 | return schema.type === "record"; 19 | } 20 | 21 | public static isArrayType(schema: Type): schema is ArrayType { 22 | if (typeof schema === "string" || schema instanceof Array) { 23 | return false; 24 | } 25 | return schema.type === "array"; 26 | } 27 | 28 | public static isMapType(schema: Type): schema is MapType { 29 | if (typeof schema === "string" || schema instanceof Array) { 30 | return false; 31 | } 32 | return schema.type === "map"; 33 | } 34 | 35 | public static isEnumType(schema: Type): schema is EnumType { 36 | if (typeof schema === "string" || schema instanceof Array) { 37 | return false; 38 | } 39 | return schema.type === "enum"; 40 | } 41 | 42 | public static isUnion(schema: Type): schema is NamedType[] { 43 | return schema instanceof Array; 44 | } 45 | 46 | public static isLogicalType(schema: Type): schema is LogicalType { 47 | if (typeof schema === "string" || schema instanceof Array) { 48 | return false; 49 | } 50 | return "logicalType" in schema; 51 | } 52 | 53 | public static isOptional(schema: Type): boolean { 54 | if (TypeHelper.isUnion(schema)) { 55 | const t1 = schema[0]; 56 | 57 | if (typeof t1 === "string") { 58 | return t1 === "null"; 59 | } 60 | } 61 | 62 | return false; 63 | } 64 | 65 | public static hasDefault(field: Field): boolean { 66 | if (field.default === undefined) { 67 | return false; 68 | } 69 | 70 | return true; 71 | } 72 | 73 | public static getDefault(field: Field): string | number | boolean | any[] | null { 74 | if (field.default === undefined) { 75 | return false; 76 | } 77 | 78 | if (field.default === "") { 79 | return `""`; 80 | } 81 | 82 | if (Array.isArray(field.default) && field.default.length === 0) { 83 | return `[]`; 84 | } 85 | 86 | return field.default; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./components/Avro/AvroRecord"; 2 | export * from "./components/Avro/BaseAvroRecord"; 3 | export * from "./components/Avro/KafkaAvroMessage"; 4 | export * from "./components/Avro/serde/BaseKafkaAvroSerde"; 5 | export * from "./components/Avro/serde/KafkaAvroDeserializer"; 6 | export * from "./components/Avro/serde/KafkaAvroFactory"; 7 | export * from "./components/Avro/serde/KafkaAvroSerializer"; 8 | export * from "./components/Avro/serde/Serializable"; 9 | export * from "./components/Compiler/Compiler"; 10 | export * from "./components/Converters/ClassConverter"; 11 | export * from "./components/Converters/EnumConverter"; 12 | export * from "./components/Converters/LogicalTypeConverter"; 13 | export * from "./components/Converters/PrimitiveConverter"; 14 | export * from "./components/Converters/RecordConverter"; 15 | export * from "./interfaces/AvroSchema"; 16 | export * from "./interfaces/CompilerOutput"; 17 | -------------------------------------------------------------------------------- /src/interfaces/AvroSchema.ts: -------------------------------------------------------------------------------- 1 | export interface AvroSchema { 2 | type: TypeNames; 3 | } 4 | 5 | export type Type = NameOrType | NameOrType[]; 6 | export type NameOrType = TypeNames | RecordType | ArrayType | NamedType | LogicalType; 7 | export type TypeNames = "record" | "array" | "null" | "map" | string; 8 | 9 | export interface Field { 10 | name: string; 11 | type: Type; 12 | default?: string | number | null | boolean | any[]; 13 | } 14 | 15 | export interface RecordType extends AvroSchema { 16 | type: "record"; 17 | name: string; 18 | namespace: string; 19 | fields: Field[]; 20 | } 21 | 22 | export interface ArrayType extends AvroSchema { 23 | type: "array"; 24 | items: Type; 25 | } 26 | 27 | export interface MapType extends AvroSchema { 28 | type: "map"; 29 | values: Type; 30 | } 31 | 32 | export interface EnumType extends AvroSchema { 33 | type: "enum"; 34 | name: string; 35 | symbols: string[]; 36 | } 37 | 38 | export interface NamedType extends AvroSchema { 39 | type: string; 40 | } 41 | 42 | export interface LogicalType extends AvroSchema { 43 | type: string; 44 | logicalType: string; 45 | } 46 | -------------------------------------------------------------------------------- /src/interfaces/CompilerOutput.ts: -------------------------------------------------------------------------------- 1 | export interface CompilerOutput { 2 | class: string; 3 | dir: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/models/ExportModel.ts: -------------------------------------------------------------------------------- 1 | export class ExportModel { 2 | public name: string; 3 | public content: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/templates/typescriptClassMethodsTemplate: -------------------------------------------------------------------------------- 1 | public static readonly schema: object = {{schema}}; 2 | 3 | public static deserialize(buffer: Buffer): {{className}} { 4 | // @ts-ignore 5 | return this.internalDeserialize(buffer) as className; 6 | } 7 | 8 | public schema(): object { 9 | return {{className}}.schema; 10 | } 11 | -------------------------------------------------------------------------------- /src/templates/typescriptClassTemplate: -------------------------------------------------------------------------------- 1 | export class {{className}} extends BaseAvroRecord implements {{classInterface}}{ 2 | {{classContent}} 3 | } 4 | -------------------------------------------------------------------------------- /test/components/Compiler.ts: -------------------------------------------------------------------------------- 1 | import * as chai from "chai"; 2 | import * as fs from "fs-extra"; 3 | import * as path from "path"; 4 | import { Compiler } from "../../src"; 5 | 6 | const expect = chai.expect; 7 | 8 | chai.should(); 9 | 10 | const dataFolder = path.resolve(`${__dirname}/../../../test/data/`); 11 | const avroFolder = path.resolve(dataFolder + `/avro/`); 12 | const expectedFolder = path.resolve(dataFolder + `/expected/`); 13 | const compiledFolder = path.resolve(dataFolder + `/compiled/`); 14 | 15 | describe("Testing Compiler", () => { 16 | 17 | afterEach(() => { 18 | fs.removeSync(compiledFolder + "/com"); 19 | fs.removeSync(compiledFolder + "/BaseAvroRecord.ts"); 20 | }); 21 | 22 | it(`should create User class when given User avro schema`, async () => { 23 | 24 | const avro = `${avroFolder}/User.avsc`; 25 | const compiledFile = `${compiledFolder}/com/example/avro/User.ts`; 26 | const expectedFile = `${expectedFolder}/User.ts.test`; 27 | 28 | const compiler = new Compiler(compiledFolder); 29 | 30 | await compiler.compile(avro); 31 | 32 | const actual = fs.readFileSync(compiledFile).toString(); 33 | const expected = fs.readFileSync(expectedFile).toString(); 34 | expect(actual).to.deep.equal(expected); 35 | }); 36 | 37 | it(`should create TradeCollection class when given TradeCollection avro schema`, async () => { 38 | 39 | const avro = `${avroFolder}/TradeCollection.avsc`; 40 | const compiledFile = `${compiledFolder}/com/example/avro/TradeCollection.ts`; 41 | const expectedFile = `${expectedFolder}/TradeCollection.ts.test`; 42 | 43 | const compiler = new Compiler(compiledFolder); 44 | 45 | await compiler.compile(avro); 46 | 47 | const actual = fs.readFileSync(compiledFile).toString(); 48 | const expected = fs.readFileSync(expectedFile).toString(); 49 | expect(actual).to.deep.equal(expected); 50 | }); 51 | 52 | it(`should create BaseAvroRecord file after compiling User schema`, async () => { 53 | 54 | const avro = `${avroFolder}/User.avsc`; 55 | const compiledFile = `${compiledFolder}/BaseAvroRecord.ts`; 56 | const expectedFile = `${expectedFolder}/BaseAvroRecord.ts.test`; 57 | 58 | const compiler = new Compiler(compiledFolder); 59 | 60 | await compiler.compile(avro); 61 | 62 | const actual = fs.readFileSync(compiledFile).toString(); 63 | const expected = fs.readFileSync(expectedFile).toString(); 64 | expect(actual).to.deep.equal(expected); 65 | }); 66 | 67 | }); 68 | -------------------------------------------------------------------------------- /test/components/Converters/ClassConverter.ts: -------------------------------------------------------------------------------- 1 | import * as chai from "chai"; 2 | import * as fs from "fs"; 3 | import * as path from "path"; 4 | import { ClassConverter } from "../../../src"; 5 | 6 | const expect = chai.expect; 7 | 8 | chai.should(); 9 | 10 | const dataFolder = path.resolve(`./test/data/`); 11 | const avroFolder = path.resolve(dataFolder + `/avro/`); 12 | const compiledFolder = path.resolve(dataFolder + `/expected/`); 13 | 14 | const getExpectedResult = (file: string) => { 15 | return fs.readFileSync(file).toString(); 16 | }; 17 | 18 | describe("RecordType Converter test", () => { 19 | it("should convert User avro schema to TS class", () => { 20 | const converter = new ClassConverter(); 21 | converter.convert(`${avroFolder}/User.avsc`); 22 | 23 | const actual = converter.joinExports(); 24 | const expected = getExpectedResult(`${compiledFolder}/User.ts.test`); 25 | expect(actual).to.deep.equal(expected); 26 | }); 27 | 28 | it("should convert TradeCollection avro schema to TS class", () => { 29 | const converter = new ClassConverter(); 30 | converter.convert(`${avroFolder}/TradeCollection.avsc`); 31 | 32 | const actual = converter.joinExports(); 33 | const expected = getExpectedResult(`${compiledFolder}/TradeCollection.ts.test`); 34 | expect(actual).to.deep.equal(expected); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/components/Converters/EnumConverter.ts: -------------------------------------------------------------------------------- 1 | import * as chai from "chai"; 2 | import * as fs from "fs"; 3 | import * as path from "path"; 4 | import { EnumConverter } from "../../../src"; 5 | 6 | const expect = chai.expect; 7 | 8 | chai.should(); 9 | 10 | const dataFolder = path.resolve(`./test/data/`); 11 | const avroFolder = path.resolve(dataFolder + `/avro/`); 12 | const compiledFolder = path.resolve(dataFolder + `/expected/`); 13 | 14 | const getExpectedResult = (file: string) => { 15 | return fs.readFileSync(file).toString(); 16 | }; 17 | 18 | describe("Enum Converter", () => { 19 | 20 | it(`should successfully convert Enum avro file to TS enum`, () => { 21 | const converter = new EnumConverter(); 22 | converter.convert(`${avroFolder}/SimpleEnum.avsc`); 23 | 24 | const actual = converter.joinExports(); 25 | const expected = getExpectedResult(`${compiledFolder}/SimpleEnum.ts.test`); 26 | expect(actual).to.deep.equal(expected); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/components/Converters/PrimitiveConverter.ts: -------------------------------------------------------------------------------- 1 | import * as chai from "chai"; 2 | import { PrimitiveConverter } from "../../../src"; 3 | 4 | const expect = chai.expect; 5 | 6 | chai.should(); 7 | 8 | describe("Compile primitive types", () => { 9 | 10 | const primitivesMap: any = { 11 | number: [ 12 | "int", 13 | "long", 14 | "double", 15 | "float", 16 | ], 17 | Buffer: [ 18 | "bytes", 19 | ], 20 | null: [ 21 | "null", 22 | ], 23 | boolean: [ 24 | "boolean", 25 | ], 26 | string: [ 27 | "string", 28 | ], 29 | }; 30 | 31 | for (const expected in primitivesMap) { 32 | if (primitivesMap.hasOwnProperty(expected) === false) { 33 | continue; 34 | } 35 | 36 | for (const primitive of primitivesMap[expected]) { 37 | 38 | it(`should return ${expected} when given ${primitive}`, () => { 39 | const converter = new PrimitiveConverter(); 40 | const actual = converter.convert(primitive); 41 | 42 | expect(expected).equal(actual); 43 | }); 44 | 45 | } 46 | } 47 | }); 48 | -------------------------------------------------------------------------------- /test/components/Converters/RecordConverter.ts: -------------------------------------------------------------------------------- 1 | import * as chai from "chai"; 2 | import * as fs from "fs"; 3 | import * as path from "path"; 4 | import { RecordConverter } from "../../../src"; 5 | 6 | const expect = chai.expect; 7 | 8 | chai.should(); 9 | 10 | const dataFolder = path.resolve(`./test/data/`); 11 | const avroFolder = path.resolve(dataFolder + `/avro/`); 12 | const compiledFolder = path.resolve(dataFolder + `/expected/`); 13 | 14 | const getExpectedResult = (file: string) => { 15 | return fs.readFileSync(file).toString(); 16 | }; 17 | 18 | describe("RecordType Converter test", () => { 19 | it("should convert simple avro schema to TS interface", () => { 20 | const converter = new RecordConverter(); 21 | converter.convert(`${avroFolder}/SimpleRecord.avsc`); 22 | 23 | const actual = converter.joinExports(); 24 | const expected = getExpectedResult(`${compiledFolder}/SimpleRecord.ts.test`); 25 | expect(actual).to.deep.equal(expected); 26 | }); 27 | 28 | it("should convert simple avro schema json to TS interface", () => { 29 | const converter = new RecordConverter(); 30 | const avro = JSON.parse(fs.readFileSync(`${avroFolder}/SimpleRecord.json`).toString()); 31 | converter.convert(avro); 32 | 33 | const actual = converter.joinExports(); 34 | const expected = getExpectedResult(`${compiledFolder}/SimpleRecord.ts.test`); 35 | expect(actual).to.deep.equal(expected); 36 | }); 37 | 38 | it("should convert avro schema with interface to TS interface", () => { 39 | const converter = new RecordConverter(); 40 | converter.convert(`${avroFolder}/RecordWithInterface.avsc`); 41 | 42 | const actual = converter.joinExports(); 43 | const expected = getExpectedResult(`${compiledFolder}/RecordWithInterface.ts.test`); 44 | expect(actual).to.deep.equal(expected); 45 | }); 46 | 47 | it("should convert avro schema with MapType type to TS interface", () => { 48 | const converter = new RecordConverter(); 49 | converter.convert(`${avroFolder}/RecordWithMap.avsc`); 50 | 51 | const actual = converter.joinExports(); 52 | const expected = getExpectedResult(`${compiledFolder}/RecordWithMap.ts.test`); 53 | expect(actual).to.deep.equal(expected); 54 | }); 55 | 56 | it("should convert avro schema with all types to TS interface", () => { 57 | const converter = new RecordConverter(); 58 | converter.convert(`${avroFolder}/RecordWithUnion.avsc`); 59 | 60 | const actual = converter.joinExports(); 61 | const expected = getExpectedResult(`${compiledFolder}/RecordWithUnion.ts.test`); 62 | expect(actual).to.deep.equal(expected); 63 | }); 64 | 65 | it("should convert avro schema with logical types", () => { 66 | const converter = new RecordConverter(); 67 | converter.convert(`${avroFolder}/RecordWithLogicalTypes.avsc`); 68 | 69 | const actual = converter.joinExports(); 70 | const expected = getExpectedResult(`${compiledFolder}/RecordWithLogicalTypes.ts.test`); 71 | expect(actual).to.deep.equal(expected); 72 | }); 73 | 74 | it("should convert avro schema with mapped logical types", () => { 75 | const converter = new RecordConverter({ "timestamp-millis" : "string" }); 76 | converter.convert(`${avroFolder}/RecordWithLogicalTypes.avsc`); 77 | 78 | const actual = converter.joinExports(); 79 | const expected = getExpectedResult(`${compiledFolder}/RecordWithLogicalTypesMapped.ts.test`); 80 | expect(actual).to.deep.equal(expected); 81 | }); 82 | 83 | it("should convert avro schema with MapType type to TS interface", () => { 84 | const converter = new RecordConverter(); 85 | converter.convert(`${avroFolder}/ComplexRecord.avsc`); 86 | 87 | const actual = converter.joinExports(); 88 | const expected = getExpectedResult(`${compiledFolder}/ComplexRecord.ts.test`); 89 | expect(actual).to.deep.equal(expected); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /test/data/avro/ComplexRecord.avsc: -------------------------------------------------------------------------------- 1 | { 2 | "type": "record", 3 | "name": "User", 4 | "namespace": "com.example.avro", 5 | "doc": "This is a user record in a fictitious to-do-list management app. It supports arbitrary grouping and nesting of items, and allows you to add items by email or by tweeting.\n\nNote this app doesn't actually exist. The schema is just a demo for [Avrodoc](https://github.com/ept/avrodoc)!", 6 | "fields": [ 7 | { 8 | "name": "id", 9 | "doc": "System-assigned numeric user ID. Cannot be changed by the user.", 10 | "type": "int" 11 | }, 12 | { 13 | "name": "username", 14 | "doc": "The username chosen by the user. Can be changed by the user.", 15 | "type": "string" 16 | }, 17 | { 18 | "name": "passwordHash", 19 | "doc": "The user's password, hashed using [scrypt](http://www.tarsnap.com/scrypt.html).", 20 | "type": "string" 21 | }, 22 | { 23 | "name": "signupDate", 24 | "doc": "Timestamp (milliseconds since epoch) when the user signed up", 25 | "type": "long" 26 | }, 27 | { 28 | "name": "mapField", 29 | "type": { 30 | "type": "map", 31 | "values": { 32 | "type": 33 | "record", 34 | "name": "Foo", 35 | "fields": [ 36 | {"name": "label", "type": "string"} 37 | ] 38 | } 39 | } 40 | }, 41 | { 42 | "name": "emailAddresses", 43 | "doc": "All email addresses on the user's account", 44 | "type": { 45 | "type": "array", 46 | "items": { 47 | "type": "record", 48 | "name": "EmailAddress", 49 | "doc": "Stores details about an email address that a user has associated with their account.", 50 | "fields": [ 51 | { 52 | "name": "address", 53 | "doc": "The email address, e.g. `foo@example.com`", 54 | "type": "string" 55 | }, 56 | { 57 | "name": "verified", 58 | "doc": "true if the user has clicked the link in a confirmation email to this address.", 59 | "type": "boolean", 60 | "default": false 61 | }, 62 | { 63 | "name": "dateAdded", 64 | "doc": "Timestamp (milliseconds since epoch) when the email address was added to the account.", 65 | "type": "long" 66 | } 67 | ] 68 | } 69 | } 70 | }, 71 | { 72 | "name": "status", 73 | "doc": "Indicator of whether this authorization is currently active, or has been revoked", 74 | "type": { 75 | "type": "enum", 76 | "name": "status", 77 | "doc": "* `PENDING`: the user has started authorizing, but not yet finished\n* `ACTIVE`: the token should work\n* `DENIED`: the user declined the authorization\n* `EXPIRED`: the token used to work, but now it doesn't\n* `REVOKED`: the user has explicitly revoked the token", 78 | "symbols": ["ACTIVE", "INACTIVE"] 79 | } 80 | } 81 | ] 82 | } 83 | -------------------------------------------------------------------------------- /test/data/avro/RecordWithEnum.avsc: -------------------------------------------------------------------------------- 1 | { 2 | "type": "record", 3 | "name": "User", 4 | "namespace": "com.example.avro", 5 | "doc": "This is a user record in a fictitious to-do-list management app. It supports arbitrary grouping and nesting of items, and allows you to add items by email or by tweeting.\n\nNote this app doesn't actually exist. The schema is just a demo for [Avrodoc](https://github.com/ept/avrodoc)!", 6 | "fields": [ 7 | { 8 | "name": "id", 9 | "doc": "System-assigned numeric user ID. Cannot be changed by the user.", 10 | "type": "int" 11 | }, 12 | { 13 | "name": "username", 14 | "doc": "The username chosen by the user. Can be changed by the user.", 15 | "type": "string" 16 | }, 17 | { 18 | "name": "passwordHash", 19 | "doc": "The user's password, hashed using [scrypt](http://www.tarsnap.com/scrypt.html).", 20 | "type": "string" 21 | }, 22 | { 23 | "name": "signupDate", 24 | "doc": "Timestamp (milliseconds since epoch) when the user signed up", 25 | "type": "long" 26 | }, 27 | { 28 | "name": "status", 29 | "doc": "Indicator of whether this authorization is currently active, or has been revoked", 30 | "type": { 31 | "type": "enum", 32 | "name": "status", 33 | "doc": "* `PENDING`: the user has started authorizing, but not yet finished\n* `ACTIVE`: the token should work\n* `DENIED`: the user declined the authorization\n* `EXPIRED`: the token used to work, but now it doesn't\n* `REVOKED`: the user has explicitly revoked the token", 34 | "symbols": ["ACTIVE", "INACTIVE"] 35 | } 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /test/data/avro/RecordWithInterface.avsc: -------------------------------------------------------------------------------- 1 | { 2 | "type": "record", 3 | "name": "User", 4 | "namespace": "com.example.avro", 5 | "doc": "This is a user record in a fictitious to-do-list management app. It supports arbitrary grouping and nesting of items, and allows you to add items by email or by tweeting.\n\nNote this app doesn't actually exist. The schema is just a demo for [Avrodoc](https://github.com/ept/avrodoc)!", 6 | "fields": [ 7 | { 8 | "name": "id", 9 | "doc": "System-assigned numeric user ID. Cannot be changed by the user.", 10 | "type": "int" 11 | }, 12 | { 13 | "name": "username", 14 | "doc": "The username chosen by the user. Can be changed by the user.", 15 | "type": "string" 16 | }, 17 | { 18 | "name": "passwordHash", 19 | "doc": "The user's password, hashed using [scrypt](http://www.tarsnap.com/scrypt.html).", 20 | "type": "string" 21 | }, 22 | { 23 | "name": "signupDate", 24 | "doc": "Timestamp (milliseconds since epoch) when the user signed up", 25 | "type": "long" 26 | }, 27 | { 28 | "name": "emailAddresses", 29 | "doc": "All email addresses on the user's account", 30 | "type": { 31 | "type": "array", 32 | "items": { 33 | "type": "record", 34 | "name": "EmailAddress", 35 | "doc": "Stores details about an email address that a user has associated with their account.", 36 | "fields": [ 37 | { 38 | "name": "address", 39 | "doc": "The email address, e.g. `foo@example.com`", 40 | "type": "string" 41 | }, 42 | { 43 | "name": "verified", 44 | "doc": "true if the user has clicked the link in a confirmation email to this address.", 45 | "type": "boolean", 46 | "default": false 47 | }, 48 | { 49 | "name": "dateAdded", 50 | "doc": "Timestamp (milliseconds since epoch) when the email address was added to the account.", 51 | "type": "long" 52 | } 53 | ] 54 | } 55 | } 56 | } 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /test/data/avro/RecordWithLogicalTypes.avsc: -------------------------------------------------------------------------------- 1 | { 2 | "type": "record", 3 | "name": "Event", 4 | "namespace": "com.example.avro", 5 | "doc": "This is a user record in a fictitious to-do-list management app. It supports arbitrary grouping and nesting of items, and allows you to add items by email or by tweeting.\n\nNote this app doesn't actually exist. The schema is just a demo for [Avrodoc](https://github.com/ept/avrodoc)!", 6 | "fields": [ 7 | { 8 | "name": "id", 9 | "doc": "System-assigned numeric user ID. Cannot be changed by the user.", 10 | "type": "int" 11 | }, 12 | { 13 | "name": "createdAt", 14 | "doc": "A timestamp for when the event was created (in epoch millis)", 15 | "type": { 16 | "type": "long", 17 | "logicalType": "timestamp-millis" 18 | } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /test/data/avro/RecordWithMap.avsc: -------------------------------------------------------------------------------- 1 | { 2 | "type": "record", 3 | "name": "User", 4 | "namespace": "com.example.avro", 5 | "doc": "This is a user record in a fictitious to-do-list management app. It supports arbitrary grouping and nesting of items, and allows you to add items by email or by tweeting.\n\nNote this app doesn't actually exist. The schema is just a demo for [Avrodoc](https://github.com/ept/avrodoc)!", 6 | "fields": [ 7 | { 8 | "name": "id", 9 | "doc": "System-assigned numeric user ID. Cannot be changed by the user.", 10 | "type": "int" 11 | }, 12 | { 13 | "name": "username", 14 | "doc": "The username chosen by the user. Can be changed by the user.", 15 | "type": "string" 16 | }, 17 | { 18 | "name": "passwordHash", 19 | "doc": "The user's password, hashed using [scrypt](http://www.tarsnap.com/scrypt.html).", 20 | "type": "string" 21 | }, 22 | { 23 | "name": "signupDate", 24 | "doc": "Timestamp (milliseconds since epoch) when the user signed up", 25 | "type": "long" 26 | }, 27 | { 28 | "name": "mapField", 29 | "type": { 30 | "type": "map", 31 | "values": { 32 | "type": 33 | "record", 34 | "name": "Foo", 35 | "fields": [ 36 | {"name": "label", "type": "string"} 37 | ] 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /test/data/avro/RecordWithUnion.avsc: -------------------------------------------------------------------------------- 1 | { 2 | "type": "record", 3 | "name": "User", 4 | "namespace": "com.example.avro", 5 | "doc": "This is a user record in a fictitious to-do-list management app. It supports arbitrary grouping and nesting of items, and allows you to add items by email or by tweeting.\n\nNote this app doesn't actually exist. The schema is just a demo for [Avrodoc](https://github.com/ept/avrodoc)!", 6 | "fields": [ 7 | { 8 | "name": "id", 9 | "doc": "System-assigned numeric user ID. Cannot be changed by the user.", 10 | "type": "int" 11 | }, 12 | { 13 | "name": "username", 14 | "doc": "The username chosen by the user. Can be changed by the user.", 15 | "type": "string" 16 | }, 17 | { 18 | "name": "passwordHash", 19 | "doc": "The user's password, hashed using [scrypt](http://www.tarsnap.com/scrypt.html).", 20 | "type": "string" 21 | }, 22 | { 23 | "name": "signupDate", 24 | "doc": "Timestamp (milliseconds since epoch) when the user signed up", 25 | "type": "long" 26 | }, 27 | { 28 | "name": "unionType", 29 | "type": [ 30 | "null", 31 | "string" 32 | ], 33 | "default": null 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /test/data/avro/SimpleEnum.avsc: -------------------------------------------------------------------------------- 1 | { 2 | "type": "enum", 3 | "name": "OAuthStatus", 4 | "doc": "* `PENDING`: the user has started authorizing, but not yet finished\n* `ACTIVE`: the token should work\n* `DENIED`: the user declined the authorization\n* `EXPIRED`: the token used to work, but now it doesn't\n* `REVOKED`: the user has explicitly revoked the token", 5 | "symbols": [ 6 | "test1", 7 | "test2", 8 | "test3" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /test/data/avro/SimpleRecord.avsc: -------------------------------------------------------------------------------- 1 | { 2 | "type": "record", 3 | "name": "User", 4 | "namespace": "com.example.avro", 5 | "doc": "This is a user record in a fictitious to-do-list management app. It supports arbitrary grouping and nesting of items, and allows you to add items by email or by tweeting.\n\nNote this app doesn't actually exist. The schema is just a demo for [Avrodoc](https://github.com/ept/avrodoc)!", 6 | "fields": [ 7 | { 8 | "name": "id", 9 | "doc": "System-assigned numeric user ID. Cannot be changed by the user.", 10 | "type": "int" 11 | }, 12 | { 13 | "name": "username", 14 | "doc": "The username chosen by the user. Can be changed by the user.", 15 | "type": "string" 16 | }, 17 | { 18 | "name": "passwordHash", 19 | "doc": "The user's password, hashed using [scrypt](http://www.tarsnap.com/scrypt.html).", 20 | "type": "string" 21 | }, 22 | { 23 | "name": "signupDate", 24 | "doc": "Timestamp (milliseconds since epoch) when the user signed up", 25 | "type": "long" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /test/data/avro/SimpleRecord.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "record", 3 | "name": "User", 4 | "namespace": "com.example.avro", 5 | "doc": "This is a user record in a fictitious to-do-list management app. It supports arbitrary grouping and nesting of items, and allows you to add items by email or by tweeting.\n\nNote this app doesn't actually exist. The schema is just a demo for [Avrodoc](https://github.com/ept/avrodoc)!", 6 | "fields": [ 7 | { 8 | "name": "id", 9 | "doc": "System-assigned numeric user ID. Cannot be changed by the user.", 10 | "type": "int" 11 | }, 12 | { 13 | "name": "username", 14 | "doc": "The username chosen by the user. Can be changed by the user.", 15 | "type": "string" 16 | }, 17 | { 18 | "name": "passwordHash", 19 | "doc": "The user's password, hashed using [scrypt](http://www.tarsnap.com/scrypt.html).", 20 | "type": "string" 21 | }, 22 | { 23 | "name": "signupDate", 24 | "doc": "Timestamp (milliseconds since epoch) when the user signed up", 25 | "type": "long" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /test/data/avro/TradeCollection.avsc: -------------------------------------------------------------------------------- 1 | { 2 | "type" : "record", 3 | "name" : "TradeCollection", 4 | "namespace": "com.example.avro", 5 | "fields" : [ { 6 | "name" : "producerId", 7 | "type" : "string", 8 | "default" : "" 9 | }, { 10 | "name" : "exchange", 11 | "type" : "string", 12 | "default" : "" 13 | }, { 14 | "name" : "market", 15 | "type" : "string", 16 | "default" : "" 17 | }, { 18 | "name" : "trades", 19 | "type" : { 20 | "type" : "array", 21 | "items" : { 22 | "type" : "record", 23 | "name" : "Trade", 24 | "fields" : [ { 25 | "name" : "id", 26 | "type" : "string", 27 | "default" : "" 28 | }, { 29 | "name" : "price", 30 | "type" : "double", 31 | "default" : 0 32 | }, { 33 | "name" : "amount", 34 | "type" : "double", 35 | "default" : 0 36 | }, { 37 | "name" : "datetime", 38 | "type" : "string", 39 | "default" : "" 40 | }, { 41 | "name" : "timestamp", 42 | "type" : "long", 43 | "default" : 0 44 | }, { 45 | "name" : "type", 46 | "type" : [ "null", { 47 | "type" : "enum", 48 | "name" : "TradeType", 49 | "symbols" : [ "Market", "Limit" ] 50 | } ], 51 | "default" : null 52 | }, { 53 | "name" : "side", 54 | "type" : [ "null", { 55 | "type" : "enum", 56 | "name" : "TradeDirection", 57 | "symbols" : [ "Buy", "Sell" ] 58 | } ], 59 | "default" : null 60 | } ] 61 | } 62 | }, 63 | "default" : [ ] 64 | } ] 65 | } 66 | -------------------------------------------------------------------------------- /test/data/avro/User.avsc: -------------------------------------------------------------------------------- 1 | { 2 | "type": "record", 3 | "name": "User", 4 | "namespace": "com.example.avro", 5 | "doc": "This is a user record in a fictitious to-do-list management app. It supports arbitrary grouping and nesting of items, and allows you to add items by email or by tweeting.\n\nNote this app doesn't actually exist. The schema is just a demo for [Avrodoc](https://github.com/ept/avrodoc)!", 6 | "fields": [ 7 | { 8 | "name": "id", 9 | "doc": "System-assigned numeric user ID. Cannot be changed by the user.", 10 | "type": "int" 11 | }, 12 | { 13 | "name": "username", 14 | "doc": "The username chosen by the user. Can be changed by the user.", 15 | "type": "string" 16 | }, 17 | { 18 | "name": "passwordHash", 19 | "doc": "The user's password, hashed using [scrypt](http://www.tarsnap.com/scrypt.html).", 20 | "type": "string" 21 | }, 22 | { 23 | "name": "signupDate", 24 | "doc": "Timestamp (milliseconds since epoch) when the user signed up", 25 | "type": "long" 26 | }, 27 | { 28 | "name": "emailAddresses", 29 | "doc": "All email addresses on the user's account", 30 | "type": { 31 | "type": "array", 32 | "items": { 33 | "type": "record", 34 | "name": "EmailAddress", 35 | "doc": "Stores details about an email address that a user has associated with their account.", 36 | "fields": [ 37 | { 38 | "name": "address", 39 | "doc": "The email address, e.g. `foo@example.com`", 40 | "type": "string" 41 | }, 42 | { 43 | "name": "verified", 44 | "doc": "true if the user has clicked the link in a confirmation email to this address.", 45 | "type": "boolean", 46 | "default": false 47 | }, 48 | { 49 | "name": "dateAdded", 50 | "doc": "Timestamp (milliseconds since epoch) when the email address was added to the account.", 51 | "type": "long" 52 | }, 53 | { 54 | "name": "dateBounced", 55 | "doc": "Timestamp (milliseconds since epoch) when an email sent to this address last bounced. Reset to null when the address no longer bounces.", 56 | "type": ["null", "long"] 57 | } 58 | ] 59 | } 60 | } 61 | }, 62 | { 63 | "name": "twitterAccounts", 64 | "doc": "All Twitter accounts that the user has OAuthed", 65 | "type": { 66 | "type": "array", 67 | "items": { 68 | "type": "record", 69 | "name": "TwitterAccount", 70 | "doc": "Stores access credentials for one Twitter account, as granted to us by the user by OAuth.", 71 | "fields": [ 72 | { 73 | "name": "status", 74 | "doc": "Indicator of whether this authorization is currently active, or has been revoked", 75 | "type": { 76 | "type": "enum", 77 | "name": "OAuthStatus", 78 | "doc": "* `PENDING`: the user has started authorizing, but not yet finished\n* `ACTIVE`: the token should work\n* `DENIED`: the user declined the authorization\n* `EXPIRED`: the token used to work, but now it doesn't\n* `REVOKED`: the user has explicitly revoked the token", 79 | "symbols": ["PENDING", "ACTIVE", "DENIED", "EXPIRED", "REVOKED"] 80 | } 81 | }, 82 | { 83 | "name": "userId", 84 | "doc": "Twitter's numeric ID for this user", 85 | "type": "long" 86 | }, 87 | { 88 | "name": "screenName", 89 | "doc": "The twitter username for this account (can be changed by the user)", 90 | "type": "string" 91 | }, 92 | { 93 | "name": "oauthToken", 94 | "doc": "The OAuth token for this Twitter account", 95 | "type": "string" 96 | }, 97 | { 98 | "name": "oauthTokenSecret", 99 | "doc": "The OAuth secret, used for signing requests on behalf of this Twitter account. `null` whilst the OAuth flow is not yet complete.", 100 | "type": ["null", "string"] 101 | }, 102 | { 103 | "name": "dateAuthorized", 104 | "doc": "Timestamp (milliseconds since epoch) when the user last authorized this Twitter account", 105 | "type": "long" 106 | } 107 | ] 108 | } 109 | } 110 | }, 111 | { 112 | "name": "toDoItems", 113 | "doc": "The top-level items in the user's to-do list", 114 | "type": { 115 | "type": "array", 116 | "items": { 117 | "type": "record", 118 | "name": "ToDoItem", 119 | "doc": "A record is one node in a To-Do item tree (every record can contain nested sub-records).", 120 | "fields": [ 121 | { 122 | "name": "status", 123 | "doc": "User-selected state for this item (e.g. whether or not it is marked as done)", 124 | "type": { 125 | "type": "enum", 126 | "name": "ToDoStatus", 127 | "doc": "* `HIDDEN`: not currently visible, e.g. because it becomes actionable in future\n* `ACTIONABLE`: appears in the current to-do list\n* `DONE`: marked as done, but still appears in the list\n* `ARCHIVED`: marked as done and no longer visible\n* `DELETED`: not done and removed from list (preserved for undo purposes)", 128 | "symbols": ["HIDDEN", "ACTIONABLE", "DONE", "ARCHIVED", "DELETED"] 129 | } 130 | }, 131 | { 132 | "name": "title", 133 | "doc": "One-line summary of the item", 134 | "type": "string" 135 | }, 136 | { 137 | "name": "description", 138 | "doc": "Detailed description (may contain HTML markup)", 139 | "type": ["null", "string"] 140 | }, 141 | { 142 | "name": "snoozeDate", 143 | "doc": "Timestamp (milliseconds since epoch) at which the item should go from `HIDDEN` to `ACTIONABLE` status", 144 | "type": ["null", "long"] 145 | }, 146 | { 147 | "name": "subItems", 148 | "doc": "List of children of this to-do tree node", 149 | "type": { 150 | "type": "array", 151 | "items": "ToDoItem" 152 | } 153 | } 154 | ] 155 | } 156 | } 157 | } 158 | ] 159 | } 160 | -------------------------------------------------------------------------------- /test/data/expected/BaseAvroRecord.ts.test: -------------------------------------------------------------------------------- 1 | export { BaseAvroRecord } from "@degordian/avro-to-typescript"; 2 | -------------------------------------------------------------------------------- /test/data/expected/ComplexRecord.ts.test: -------------------------------------------------------------------------------- 1 | export interface Foo { 2 | label: string; 3 | } 4 | 5 | export interface EmailAddress { 6 | address: string; 7 | verified: boolean; 8 | dateAdded: number; 9 | } 10 | 11 | export interface User { 12 | id: number; 13 | username: string; 14 | passwordHash: string; 15 | signupDate: number; 16 | mapField: { [index: string]: Foo }; 17 | emailAddresses: EmailAddress[]; 18 | status: status; 19 | } 20 | -------------------------------------------------------------------------------- /test/data/expected/RecordWithEnum.ts.test: -------------------------------------------------------------------------------- 1 | export interface User { 2 | id: number; 3 | username: string; 4 | passwordHash: string; 5 | signupDate: number; 6 | status: status; 7 | } 8 | -------------------------------------------------------------------------------- /test/data/expected/RecordWithInterface.ts.test: -------------------------------------------------------------------------------- 1 | export interface EmailAddress { 2 | address: string; 3 | verified: boolean; 4 | dateAdded: number; 5 | } 6 | 7 | export interface User { 8 | id: number; 9 | username: string; 10 | passwordHash: string; 11 | signupDate: number; 12 | emailAddresses: EmailAddress[]; 13 | } 14 | -------------------------------------------------------------------------------- /test/data/expected/RecordWithLogicalTypes.ts.test: -------------------------------------------------------------------------------- 1 | export interface Event { 2 | id: number; 3 | createdAt: number; 4 | } 5 | -------------------------------------------------------------------------------- /test/data/expected/RecordWithLogicalTypesMapped.ts.test: -------------------------------------------------------------------------------- 1 | export interface Event { 2 | id: number; 3 | createdAt: string; 4 | } 5 | -------------------------------------------------------------------------------- /test/data/expected/RecordWithMap.ts.test: -------------------------------------------------------------------------------- 1 | export interface Foo { 2 | label: string; 3 | } 4 | 5 | export interface User { 6 | id: number; 7 | username: string; 8 | passwordHash: string; 9 | signupDate: number; 10 | mapField: { [index: string]: Foo }; 11 | } 12 | -------------------------------------------------------------------------------- /test/data/expected/RecordWithUnion.ts.test: -------------------------------------------------------------------------------- 1 | export interface User { 2 | id: number; 3 | username: string; 4 | passwordHash: string; 5 | signupDate: number; 6 | unionType?: null | string; 7 | } 8 | -------------------------------------------------------------------------------- /test/data/expected/SimpleEnum.ts.test: -------------------------------------------------------------------------------- 1 | export enum OAuthStatus { 2 | test1, 3 | test2, 4 | test3, 5 | } 6 | -------------------------------------------------------------------------------- /test/data/expected/SimpleRecord.ts.test: -------------------------------------------------------------------------------- 1 | export interface User { 2 | id: number; 3 | username: string; 4 | passwordHash: string; 5 | signupDate: number; 6 | } 7 | -------------------------------------------------------------------------------- /test/data/expected/TradeCollection.ts.test: -------------------------------------------------------------------------------- 1 | // tslint:disable 2 | import { BaseAvroRecord } from "../../../BaseAvroRecord"; 3 | import { TradeType } from "./TradeTypeEnum"; 4 | import { TradeDirection } from "./TradeDirectionEnum"; 5 | 6 | export interface Trade { 7 | id: string; 8 | price: number; 9 | amount: number; 10 | datetime: string; 11 | timestamp: number; 12 | type?: null | TradeType; 13 | side?: null | TradeDirection; 14 | } 15 | 16 | export interface TradeCollectionInterface { 17 | producerId: string; 18 | exchange: string; 19 | market: string; 20 | trades: Trade[]; 21 | } 22 | 23 | export class TradeCollection extends BaseAvroRecord implements TradeCollectionInterface { 24 | 25 | public static readonly subject: string = "TradeCollection"; 26 | public static readonly schema: object = { 27 | "type": "record", 28 | "name": "TradeCollection", 29 | "namespace": "com.example.avro", 30 | "fields": [ 31 | { 32 | "name": "producerId", 33 | "type": "string", 34 | "default": "" 35 | }, 36 | { 37 | "name": "exchange", 38 | "type": "string", 39 | "default": "" 40 | }, 41 | { 42 | "name": "market", 43 | "type": "string", 44 | "default": "" 45 | }, 46 | { 47 | "name": "trades", 48 | "type": { 49 | "type": "array", 50 | "items": { 51 | "type": "record", 52 | "name": "Trade", 53 | "fields": [ 54 | { 55 | "name": "id", 56 | "type": "string", 57 | "default": "" 58 | }, 59 | { 60 | "name": "price", 61 | "type": "double", 62 | "default": 0 63 | }, 64 | { 65 | "name": "amount", 66 | "type": "double", 67 | "default": 0 68 | }, 69 | { 70 | "name": "datetime", 71 | "type": "string", 72 | "default": "" 73 | }, 74 | { 75 | "name": "timestamp", 76 | "type": "long", 77 | "default": 0 78 | }, 79 | { 80 | "name": "type", 81 | "type": [ 82 | "null", 83 | { 84 | "type": "enum", 85 | "name": "TradeType", 86 | "symbols": [ 87 | "Market", 88 | "Limit" 89 | ] 90 | } 91 | ], 92 | "default": null 93 | }, 94 | { 95 | "name": "side", 96 | "type": [ 97 | "null", 98 | { 99 | "type": "enum", 100 | "name": "TradeDirection", 101 | "symbols": [ 102 | "Buy", 103 | "Sell" 104 | ] 105 | } 106 | ], 107 | "default": null 108 | } 109 | ] 110 | } 111 | }, 112 | "default": [] 113 | } 114 | ] 115 | } 116 | 117 | public static deserialize(buffer: Buffer, newSchema?: object): TradeCollection { 118 | const result = new TradeCollection(); 119 | const rawResult = this.internalDeserialize(buffer, newSchema); 120 | result.loadValuesFromType(rawResult); 121 | 122 | return result; 123 | } 124 | 125 | public producerId: string = ""; 126 | public exchange: string = ""; 127 | public market: string = ""; 128 | public trades: Trade[] = []; 129 | 130 | public schema(): object { 131 | return TradeCollection.schema; 132 | } 133 | 134 | public subject(): string { 135 | return TradeCollection.subject; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /test/data/expected/User.ts.test: -------------------------------------------------------------------------------- 1 | // tslint:disable 2 | import { BaseAvroRecord } from "../../../BaseAvroRecord"; 3 | import { OAuthStatus } from "./OAuthStatusEnum"; 4 | import { ToDoStatus } from "./ToDoStatusEnum"; 5 | 6 | export interface EmailAddress { 7 | address: string; 8 | verified: boolean; 9 | dateAdded: number; 10 | dateBounced?: null | number; 11 | } 12 | 13 | export interface TwitterAccount { 14 | status: OAuthStatus; 15 | userId: number; 16 | screenName: string; 17 | oauthToken: string; 18 | oauthTokenSecret?: null | string; 19 | dateAuthorized: number; 20 | } 21 | 22 | export interface ToDoItem { 23 | status: ToDoStatus; 24 | title: string; 25 | description?: null | string; 26 | snoozeDate?: null | number; 27 | subItems: any[]; 28 | } 29 | 30 | export interface UserInterface { 31 | id: number; 32 | username: string; 33 | passwordHash: string; 34 | signupDate: number; 35 | emailAddresses: EmailAddress[]; 36 | twitterAccounts: TwitterAccount[]; 37 | toDoItems: ToDoItem[]; 38 | } 39 | 40 | export class User extends BaseAvroRecord implements UserInterface { 41 | 42 | public static readonly subject: string = "User"; 43 | public static readonly schema: object = { 44 | "type": "record", 45 | "name": "User", 46 | "namespace": "com.example.avro", 47 | "doc": "This is a user record in a fictitious to-do-list management app. It supports arbitrary grouping and nesting of items, and allows you to add items by email or by tweeting.\n\nNote this app doesn't actually exist. The schema is just a demo for [Avrodoc](https://github.com/ept/avrodoc)!", 48 | "fields": [ 49 | { 50 | "name": "id", 51 | "doc": "System-assigned numeric user ID. Cannot be changed by the user.", 52 | "type": "int" 53 | }, 54 | { 55 | "name": "username", 56 | "doc": "The username chosen by the user. Can be changed by the user.", 57 | "type": "string" 58 | }, 59 | { 60 | "name": "passwordHash", 61 | "doc": "The user's password, hashed using [scrypt](http://www.tarsnap.com/scrypt.html).", 62 | "type": "string" 63 | }, 64 | { 65 | "name": "signupDate", 66 | "doc": "Timestamp (milliseconds since epoch) when the user signed up", 67 | "type": "long" 68 | }, 69 | { 70 | "name": "emailAddresses", 71 | "doc": "All email addresses on the user's account", 72 | "type": { 73 | "type": "array", 74 | "items": { 75 | "type": "record", 76 | "name": "EmailAddress", 77 | "doc": "Stores details about an email address that a user has associated with their account.", 78 | "fields": [ 79 | { 80 | "name": "address", 81 | "doc": "The email address, e.g. `foo@example.com`", 82 | "type": "string" 83 | }, 84 | { 85 | "name": "verified", 86 | "doc": "true if the user has clicked the link in a confirmation email to this address.", 87 | "type": "boolean", 88 | "default": false 89 | }, 90 | { 91 | "name": "dateAdded", 92 | "doc": "Timestamp (milliseconds since epoch) when the email address was added to the account.", 93 | "type": "long" 94 | }, 95 | { 96 | "name": "dateBounced", 97 | "doc": "Timestamp (milliseconds since epoch) when an email sent to this address last bounced. Reset to null when the address no longer bounces.", 98 | "type": [ 99 | "null", 100 | "long" 101 | ] 102 | } 103 | ] 104 | } 105 | } 106 | }, 107 | { 108 | "name": "twitterAccounts", 109 | "doc": "All Twitter accounts that the user has OAuthed", 110 | "type": { 111 | "type": "array", 112 | "items": { 113 | "type": "record", 114 | "name": "TwitterAccount", 115 | "doc": "Stores access credentials for one Twitter account, as granted to us by the user by OAuth.", 116 | "fields": [ 117 | { 118 | "name": "status", 119 | "doc": "Indicator of whether this authorization is currently active, or has been revoked", 120 | "type": { 121 | "type": "enum", 122 | "name": "OAuthStatus", 123 | "doc": "* `PENDING`: the user has started authorizing, but not yet finished\n* `ACTIVE`: the token should work\n* `DENIED`: the user declined the authorization\n* `EXPIRED`: the token used to work, but now it doesn't\n* `REVOKED`: the user has explicitly revoked the token", 124 | "symbols": [ 125 | "PENDING", 126 | "ACTIVE", 127 | "DENIED", 128 | "EXPIRED", 129 | "REVOKED" 130 | ] 131 | } 132 | }, 133 | { 134 | "name": "userId", 135 | "doc": "Twitter's numeric ID for this user", 136 | "type": "long" 137 | }, 138 | { 139 | "name": "screenName", 140 | "doc": "The twitter username for this account (can be changed by the user)", 141 | "type": "string" 142 | }, 143 | { 144 | "name": "oauthToken", 145 | "doc": "The OAuth token for this Twitter account", 146 | "type": "string" 147 | }, 148 | { 149 | "name": "oauthTokenSecret", 150 | "doc": "The OAuth secret, used for signing requests on behalf of this Twitter account. `null` whilst the OAuth flow is not yet complete.", 151 | "type": [ 152 | "null", 153 | "string" 154 | ] 155 | }, 156 | { 157 | "name": "dateAuthorized", 158 | "doc": "Timestamp (milliseconds since epoch) when the user last authorized this Twitter account", 159 | "type": "long" 160 | } 161 | ] 162 | } 163 | } 164 | }, 165 | { 166 | "name": "toDoItems", 167 | "doc": "The top-level items in the user's to-do list", 168 | "type": { 169 | "type": "array", 170 | "items": { 171 | "type": "record", 172 | "name": "ToDoItem", 173 | "doc": "A record is one node in a To-Do item tree (every record can contain nested sub-records).", 174 | "fields": [ 175 | { 176 | "name": "status", 177 | "doc": "User-selected state for this item (e.g. whether or not it is marked as done)", 178 | "type": { 179 | "type": "enum", 180 | "name": "ToDoStatus", 181 | "doc": "* `HIDDEN`: not currently visible, e.g. because it becomes actionable in future\n* `ACTIONABLE`: appears in the current to-do list\n* `DONE`: marked as done, but still appears in the list\n* `ARCHIVED`: marked as done and no longer visible\n* `DELETED`: not done and removed from list (preserved for undo purposes)", 182 | "symbols": [ 183 | "HIDDEN", 184 | "ACTIONABLE", 185 | "DONE", 186 | "ARCHIVED", 187 | "DELETED" 188 | ] 189 | } 190 | }, 191 | { 192 | "name": "title", 193 | "doc": "One-line summary of the item", 194 | "type": "string" 195 | }, 196 | { 197 | "name": "description", 198 | "doc": "Detailed description (may contain HTML markup)", 199 | "type": [ 200 | "null", 201 | "string" 202 | ] 203 | }, 204 | { 205 | "name": "snoozeDate", 206 | "doc": "Timestamp (milliseconds since epoch) at which the item should go from `HIDDEN` to `ACTIONABLE` status", 207 | "type": [ 208 | "null", 209 | "long" 210 | ] 211 | }, 212 | { 213 | "name": "subItems", 214 | "doc": "List of children of this to-do tree node", 215 | "type": { 216 | "type": "array", 217 | "items": "ToDoItem" 218 | } 219 | } 220 | ] 221 | } 222 | } 223 | } 224 | ] 225 | } 226 | 227 | public static deserialize(buffer: Buffer, newSchema?: object): User { 228 | const result = new User(); 229 | const rawResult = this.internalDeserialize(buffer, newSchema); 230 | result.loadValuesFromType(rawResult); 231 | 232 | return result; 233 | } 234 | 235 | public id!: number; 236 | public username!: string; 237 | public passwordHash!: string; 238 | public signupDate!: number; 239 | public emailAddresses!: EmailAddress[]; 240 | public twitterAccounts!: TwitterAccount[]; 241 | public toDoItems!: ToDoItem[]; 242 | 243 | public schema(): object { 244 | return User.schema; 245 | } 246 | 247 | public subject(): string { 248 | return User.subject; 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@degordian/standards/tsconfig.json", 3 | "compilerOptions": { 4 | "skipLibCheck" : true, 5 | "outDir": "dist", /* Redirect output structure to the directory. */ 6 | "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 7 | "typeRoots": ["node_modules/@types", "src/types"], /* List of folders to include type definitions from. */ 8 | "sourceRoot": "src", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 9 | "experimentalDecorators": true, 10 | "declaration": true 11 | }, 12 | "include": [ 13 | "./src/**/*.ts", 14 | "./test/**/*.ts" 15 | ], 16 | "exclude": [ 17 | "node_modules" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "@degordian/standards/tslint", 5 | "tslint-sonarts" 6 | ], 7 | "jsRules": {}, 8 | "rules": { 9 | "no-console": false, 10 | "no-commented-code": false, 11 | "object-literal-key-quotes": false, 12 | "no-trailing-whitespace": true 13 | }, 14 | "rulesDirectory": [] 15 | } 16 | --------------------------------------------------------------------------------