{"id":29410635,"url":"https://github.com/voronov-maxim/tstoodata","last_synced_at":"2025-07-11T07:02:46.746Z","repository":{"id":57381372,"uuid":"240959823","full_name":"voronov-maxim/TsToOdata","owner":"voronov-maxim","description":"Typescript OData queries in a fluent way like linq.","archived":false,"fork":false,"pushed_at":"2020-04-19T16:42:38.000Z","size":220,"stargazers_count":12,"open_issues_count":2,"forks_count":1,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-06-21T17:45:41.780Z","etag":null,"topics":["babel-plugin","client","fluent","linq","odata","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/voronov-maxim.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-02-16T20:20:52.000Z","updated_at":"2024-02-10T02:09:15.000Z","dependencies_parsed_at":"2022-09-26T16:50:28.863Z","dependency_job_id":null,"html_url":"https://github.com/voronov-maxim/TsToOdata","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/voronov-maxim/TsToOdata","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/voronov-maxim%2FTsToOdata","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/voronov-maxim%2FTsToOdata/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/voronov-maxim%2FTsToOdata/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/voronov-maxim%2FTsToOdata/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/voronov-maxim","download_url":"https://codeload.github.com/voronov-maxim/TsToOdata/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/voronov-maxim%2FTsToOdata/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":264752274,"owners_count":23658590,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["babel-plugin","client","fluent","linq","odata","typescript"],"created_at":"2025-07-11T07:01:04.951Z","updated_at":"2025-07-11T07:02:46.734Z","avatar_url":"https://github.com/voronov-maxim.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"#### TsToOdata ####  \nTypescript OData queries in a fluent way like linq.\n\n#### How to install ####  \n```\nnpm install --save-dev @babel/core @babel/cli\nnpm install ts2odata\nnpm install --save-dev babel-plugin-ts2odata\n```\nbabel.config.js\n```javascript\nmodule.exports = {\n    plugins: [\n        [\n            'babel-plugin-ts2odata',\n            {\n                odataNamespace: 'OdataToEntity.Test.Model'\n            }\n        ]\n    ]\n};\n```\nFor odatanamespace option see Enumeration types section.\n\n#### Create data model ####  \nCreate Json schema from OData EDMX.  \nTo do this, you can use the library [OdataToEntity](https://github.com/voronov-maxim/OdataToEntity/wiki/Json-schema).\n```cs\nIEdmModel edmModel;\nusing (var reader = XmlReader.Create(\"edmx_schema.xml\"))\n    edmModel = CsdlReader.Parse(reader);\n\nvar generator = new OeJsonSchemaGenerator(edmModel);\nusing (var utf8Json = new MemoryStream())\n{\n    generator.Generate(utf8Json);\n    utf8Json.Position = 0;\n    File.WriteAllBytes(\"json_schema.json\", utf8Json.ToArray());\n}\n```\nCreate TypeScript classes from Json schema.\nTo do this, you can use the library [quicktype](https://github.com/quicktype/quicktype).  \nThe result is a [data model](https://raw.githubusercontent.com/voronov-maxim/TsToOdata/master/test/order.ts), which I will use in below mentioned examples.\n\n#### Create data access context ####  \n```javascript\nimport { EntitySet, OdataContext } from 'ts2odata';\nimport * as oe from './order';\n\nexport class OrderContext extends OdataContext\u003cOrderContext\u003e {\n    Categories = EntitySet.default\u003coe.Category\u003e();\n    Customers = EntitySet.default\u003coe.Customer\u003e();\n    OrderItems = EntitySet.default\u003coe.OrderItem\u003e();\n    OrderItemsView = EntitySet.default\u003coe.OrderItemsView\u003e();\n    Orders = EntitySet.default\u003coe.Order\u003e();\n}\n```\n```javascript\nlet context: OrderContext = OdataContext.create(OrderContext, 'http://localhost:5000/api');\n```\n\n#### Query examples ####  \nGet all entities\n```javascript\ncontext.Orders;\n//http://localhost:5000/api/Orders\n```\nSelects a subset of properties\n```javascript\ncontext.Orders.select(o =\u003e { return { p: o.Name } });\n//http://localhost:5000/api/Orders?$select=Name\n```\nSort ascending\n```javascript\ncontext.Orders.orderby(i =\u003e i.Id);\n//http://localhost:5000/api/Orders?$orderby=Id\n```\nSort descending\n```javascript\ncontext.Orders.orderbyDescending(i =\u003e i.Id);\n//http://localhost:5000/api/Orders?$orderby=Id desc\n```\nFilter expressions\n```javascript\ncontext.Orders.filter(o =\u003e o.Date.getFullYear() == 2016);\n//http://localhost:5000/api/Orders?$filter=year(Date) eq 2016\n```\nGet related entities \n```javascript\ncontext.Orders.expand(o =\u003e o.Items);\n//http://localhost:5000/api/Orders?$expand=Items\n```\nGet related entities nested levels\n```javascript\ncontext.Customers.expand(c =\u003e c.Orders).thenExpand(o =\u003e o.Items);\n//http://localhost:5000/api/Customers?$expand=Orders($expand=Items)\n```\nSkip a subset of the entities\n```javascript\ncontext.Orders.orderby(i =\u003e i.Id).skip(2);\n//http://localhost:5000/api/Orders?$orderby=Id\u0026$skip=2\n```\nTake a subset of the entities\n```javascript\ncontext.Orders.orderby(i =\u003e i.Id).top(3);\n//http://localhost:5000/api/Orders?$orderby=Id\u0026$top=3\n```\nGroups of entities\n```javascript\ncontext.OrderItems.groupby(i =\u003e { return { Product: i.Product } });\n//localhost:5000/api/OrderItems?$apply=groupby((Product))\n```\nAggregate functions\n```javascript\ncontext.OrderItems.groupby(i =\u003e { return { OrderId: i.OrderId, Status: i.Order.Status } })\n    .select(g =\u003e {\n        return {\n            orderId: g.key.OrderId,\n            avg: g.average(i =\u003e i.Price),\n            dcnt: g.countdistinct(i =\u003e i.Product),\n            max: g.max(i =\u003e i.Price),\n            max_status: g.max(_ =\u003e g.key.Status),\n            min: g.min(i =\u003e i.Price),\n            sum: g.sum(i =\u003e i.Price),\n            cnt: g.count()\n        }});\n//http://localhost:5000/api/OrderItems?$apply=groupby((OrderId,Order/Status),aggregate(Price with average as avg,Product with countdistinct as dcnt,Price with max as max,Order/Status with max as max_status,Price with min as min,Price with sum as sum,$count as cnt))\n```\nGet entity by key properties\n```javascript\ncontext.Customers.key({ Country: 'RU', Id: 1 });\n//http://localhost:5000/api/Customers(Country='RU',Id=1)\n```\nGet entity by key properties to another related entity\n```javascript\ncontext.OrderItems.key(1, i =\u003e i.Order.Customer);\n//http://localhost:5000/api/OrderItems(1)/Order/Customer\n```\nCompute properties\n```javascript\ncontext.OrderItems\n    .select(i =\u003e {\n        return {\n            product: i.Product,\n            Total: i.Count * i.Price,\n            SumId: i.Id + i.OrderId\n        }\n    });\n//http://localhost:5000/api/OrderItems?$select=Product\u0026$compute=Count mul Price as Total,Id add OrderId as SumId\n```\nLambda operators\n```javascript\ncontext.Orders.filter(o =\u003e o.Items.every(i =\u003e i.Price \u003e= 2.1));\n//http://localhost:5000/api/Orders?$filter=Items/all(d:d/Price ge 2.1)\n```\n```javascript\ncontext.Orders.filter(o =\u003e o.Items.some(i =\u003e i.Count \u003e 2));\n//http://localhost:5000/api/Orders?$filter=Items/any(d:d/Count gt 2)\n```\nIN operator\n```javascript\nlet items = [1.1, 1.2, 1.3];\ncontext.OrderItems.filter(i =\u003e items.includes(i.Price), { items: items });\n//http://localhost:5000/api/OrderItems?$filter=Price in (1.1,1.2,1.3)\n```\nCount of entities\n```javascript\ncontext.Orders.count();\n//http://localhost:5000/api/Orders/$count\n```\nRevert context to entity set  \nApply *asEntitySet* method when must to sort by properties missing in the selection set\n```javascript\ncontext.Orders(o =\u003e o.AltCustomer).thenSelect(o =\u003e {{\n    p1: o.Address,\n    : o.Country,\n    : o.Id,\n    : o.Name,\n    : o.Sex\n}}).asEntitySet().orderby(o =\u003e o.Id)\n//http://localhost:5000/api/Orders?$expand=AltCustomer($select=Address,Country,Id,Name,Sex)\u0026$orderby=Id\n```\nOther examples are on the [GitHub](https://github.com/voronov-maxim/TsToOdata/blob/master/test/QueryTests.ts).\n\nIt should be noted that the methods *select*, *expand*, *groupby* change context, their result is a new type and to continue execution in this new context, you need to use methods with prefix *then*: *thenFilter*, *thenExpand*, *thenOrderby*, *thenOrderbyDescending*, *thenSkip*, *thenTop*. The *select* and *thenSelect* methods irreversibly change the context, and to continue execution in parent context, you need to use the *asEntitySet* method.\n\n#### Parameterized query ####  \n*filter/thenFilter*,*select/thenSelect*, *groupby* methods can be parameterized, the names of the properties of the parameterization object must match the names of the variables in the query.\n```javascript\nlet count: number | null = null;\ncontext.OrderItems.filter(i =\u003e i.Count == count, { count: count });  \n//http://localhost:5000/api/OrderItems?$filter=Count eq null\n```\n```javascript\nlet s = {\n    altCustomerId: 3,\n    customerId: 4,\n    dateYear: 2016,\n    dateMonth: 11,\n    dateDay: 20,\n    date: null,\n    name: 'unknown',\n    status: \"OdataToEntity.Test.Model.OrderStatus'Unknown'\",\n    count1: 0,\n    count2: null,\n    price1: 0,\n    price2: null,\n    product1: 'unknown',\n    product2: 'null',\n    orderId: -1,\n    id: 1\n};\ncontext.Orders.filter(o =\u003e o.AltCustomerId == s.altCustomerId \u0026\u0026\n\to.CustomerId == s.customerId \u0026\u0026\n\t(o.Date.getFullYear() == s.dateYear \u0026\u0026\n\t\to.Date.getMonth() \u003e s.dateMonth \u0026\u0026\n\t\to.Date.getDay() \u003c s.dateDay ||\n\t\to.Date == s.date) \u0026\u0026\n\to.Name.includes(s.name) \u0026\u0026\n\to.Status == s.status, s)\n\t.expand(o =\u003e o.Items)\n\t\t.thenFilter(i =\u003e (i.Count == s.count1 ||\n\t\t\t\ti.Count == s.count2) \u0026\u0026\n\t\t\t(i.Price == s.price1 ||\n\t\t\t\ti.Price == s.price2) \u0026\u0026\n\t\t\t(i.Product.includes(s.product1) ||\n\t\t\t\ti.Product.includes(s.product2)) \u0026\u0026\n\t\t\ti.OrderId \u003e s.orderId \u0026\u0026\n\t\t\ti.Id != s.id, s);\n/*http://localhost:5000/api/Orders?$filter=AltCustomerId eq 3 and\n\tCustomerId eq 4 and\n\t(year(Date) eq 2016 and\n\t\tmonth(Date) gt 11 and\n\t\tday(Date) lt 20 or\n\t\tDate eq null) and\n\tcontains(Name,'unknown') and\n\tStatus eq OdataToEntity.Test.Model.OrderStatus'Unknown'\u0026\n\t$expand=Items(\n\t\t$filter=(Count eq 0 or\n\t\t\tCount eq null) and\n\t\t(Price eq 0 or\n\t\t\tPrice eq null) and\n\t\t(contains(Product,'unknown') or\n\t\t\tcontains(Product,'null')) and\n\t\tOrderId gt -1 and\n\t\tId ne 1)*/\n```\n#### Functions mapping ####  \n| JavaScript    |  OData     |\n|---------------|------------|\n| Math.ceil      | ceiling     |\n| concat          | concat    |\n| includes       | contains  |\n| getDay         | day         |\n| endsWith     | endswith  |\n| Math.floor    | floor        |\n| getHours      | hour       |\n| indexOf        | indexof    |\n| stringLength | length     |\n| getMinutes   | minute    |\n| getMonth     | month     |\n| Math.round  | round      |\n| getSeconds  | second     |\n| startsWith    | startswith |\n| substring     | substring  |\n| toLowerCase | tolower   |\n| toUpperCase | toupper   |\n| trim             | trim        |\n| getFullYear   | year        |\n\nTo get the length of the string, you should use *OdataFunctions.stringLength*\n```javascript\ncontext.Customers.filter(c =\u003e OdataFunctions.stringLength(c.Name) == 5);  \n//http://localhost:5000/api/Customers?$filter=length(Name) eq 5\n```\nTo get the length of the array, you should use*OdataFunctions.arrayLength*\n```javascript\ncontext.Orders.filter(o =\u003e OdataFunctions.arrayLength(o.Items) \u003e 2);  \n//http://localhost:5000/api/Customers?$filter=Items/$count gt 2\n```\n\n#### Get URL and execute query ####  \nQuery definition methods such as *select*, *filter* and other must be ended *getQueryUrl* or *toArrayAsync*.\n*getQueryUrl* return URL. Executing this TypeScript code:\n```javascript\nlet url: URL = context.Customers\n    .expand(c =\u003e c.AltOrders).thenExpand(o =\u003e o.Items).thenOrderby(i =\u003e i.Price)\n    .expand(c =\u003e c.AltOrders).thenExpand(o =\u003e o.ShippingAddresses).thenOrderby(s =\u003e s.Id)\n    .expand(c =\u003e c.Orders).thenExpand(o =\u003e o.Items).thenOrderby(i =\u003e i.Price)\n    .expand(c =\u003e c.Orders).thenExpand(o =\u003e o.ShippingAddresses).thenOrderby(s =\u003e s.Id)\n    .orderby(c =\u003e c.Country).orderby(c =\u003e c.Id).getQueryUrl();\n```\nreturn OData query:\n```\nhttp://localhost:5000/api/Customers?$expand=\nAltOrders($expand=Items($orderby=Price),ShippingAddresses($orderby=Id)),\nOrders($expand=Items($orderby=Price),ShippingAddresses($orderby=Id))\n\u0026$orderby=Country,Id\n```\n*toArrayAsync* returns query result as Json. Executing this TypeScript code:\n```javascript\ncontext.Customers\n    .expand(c =\u003e c.Orders).thenSelect(o =\u003e { return { Date: o.Date } }).orderby(o =\u003e o.Date)\n    .asEntitySet().select(c =\u003e { return { Name: c.Name } }).orderby(c =\u003e c.Name).toArrayAsync();\n```\nreturn Json:\n```json\n[{\n\t\t\"Name\": \"Ivan\",\n\t\t\"Orders\": [{\n\t\t\t\t\"Date\": \"2016-07-04T19:10:10.8237573+03:00\"\n\t\t\t}, {\n\t\t\t\t\"Date\": \"2020-02-20T20:20:20.000002+03:00\"\n\t\t\t}\n\t\t]\n\t}, {\n\t\t\"Name\": \"Natasha\",\n\t\t\"Orders\": [{\n\t\t\t\t\"Date\": \"2016-07-04T19:10:11+03:00\"\n\t\t\t}\n\t\t]\n\t}, {\n\t\t\"Name\": \"Sasha\",\n\t\t\"Orders\": []\n\t}, {\n\t\t\"Name\": \"Unknown\",\n\t\t\"Orders\": [{\n\t\t\t\t\"Date\": null\n\t\t\t}\n\t\t]\n\t}\n]\n```\nIf you want to get the property as a date, not a string, you can call *toArrayAsync* with an optional parameter *OdataParser*:\n```javascript\nimport { OdataParser } from 'ts2odata';\nimport schema from './schema.json';\n\nlet odataParser = new OdataParser(schema);\ncontext.Orders.toArrayAsync(odataParser);\n```\n\n#### Enumeration types ####  \nIf your OData service does not support enumeration without a Namespace, for proper code translation it is necessary to pass  Namespace value to the method of creating a data context:\n```javascript\nlet context: OrderContext = OdataContext.create(OrderContext, 'http://localhost:5000/api', 'OdataToEntity.Test.Model');\n```\nIn some cases, for the correct translation of enumeration types, it may be necessary to create an object *OdataParser*.\n```javascript\nimport { OdataParser } from 'ts2odata';\nimport schema from './schema.json';\n\nlet odataParser = new OdataParser(schema);\nlet context: OrderContext = OdataContext.create(OrderContext, 'http://localhost:5000/api', 'OdataToEntity.Test.Model', odataParser);\n```\n#### Babel plugin ####  \nTypeScript code:\n```javascript\nlet price = 2.1;\nlet orders = context.Orders.filter(o =\u003e o.Items.every(i =\u003e i.Price \u003e= price), { price })\n    .select(i =\u003e { return { orderYear: i.Date.getFullYear() } }).toArrayAsync();\n```\nTranslated into:\n```javascript\nlet price = 2.1;\nlet orders = context.Orders.filter(\"Items/all(d:d/Price ge {price})\", {\n  price\n}).select(\"[{\\\"expression\\\":\\\"year(Date)\\\",\\\"alias\\\":\\\"orderYear\\\",\\\"kind\\\":1}]\").toArrayAsync();\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvoronov-maxim%2Ftstoodata","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvoronov-maxim%2Ftstoodata","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvoronov-maxim%2Ftstoodata/lists"}