>
5 |
6 | export const useResponsive = () => {
7 | const theme = useTheme()
8 |
9 | const matches = useBreakpoints();
10 |
11 | return function (responsiveValues: ResponsiveValues
) {
12 | let match: Breakpoint | undefined;
13 | theme.breakpoints.keys.forEach((breakpoint) => {
14 | if (matches[breakpoint] && responsiveValues[breakpoint] != null) {
15 | match = breakpoint;
16 | }
17 | })
18 |
19 | return match && responsiveValues[match]
20 | }
21 | }
22 |
23 | // eslint-disable-next-line react-hooks/exhaustive-deps
24 | export const useMountEffect = (func: React.EffectCallback) => useEffect(func, []);
25 |
26 | export const useBreakpoints = ():Partial> => {
27 | const theme = useTheme();
28 | const [matches, setMatches] = useState>>(getMatches(theme.breakpoints.keys, theme));
29 |
30 | useEffect(() => {
31 | const queries: Partial> = getQueries(theme.breakpoints.keys, theme);
32 | const listeners: Partial void>> = {};
33 |
34 | const updateMatch = (b: Breakpoint) => {
35 | setMatches((oldMatches) => ({...oldMatches, [b]: queries[b]?.matches ?? false }));
36 | }
37 |
38 | theme.breakpoints.keys.forEach(b => {
39 | listeners[b] = () => updateMatch(b);
40 | queries[b]!.addEventListener("change", listeners[b]!);
41 | });
42 |
43 | return () => {
44 | theme.breakpoints.keys.forEach(b => {
45 | queries[b]!.removeEventListener("change", listeners[b]!)
46 | })
47 | }
48 | }, [theme]);
49 |
50 | return matches;
51 | }
52 |
53 | const getQueries = (breakpoints: Breakpoint[], theme: Theme) => breakpoints.reduce((acc: Partial>, b) =>
54 | ({
55 | ...acc,
56 | [b]: window.matchMedia(theme.breakpoints.up(b).replace(/^@media( ?)/m, ''))
57 | }),
58 | {}
59 | );
60 |
61 | const getMatches = (breakpoints: Breakpoint[], theme: Theme) => breakpoints.reduce((acc: Partial>, b) =>
62 | ({
63 | ...acc,
64 | [b]: window.matchMedia(theme.breakpoints.up(b).replace(/^@media( ?)/m, '')).matches
65 | }),
66 | {}
67 | );
--------------------------------------------------------------------------------
/packages/base/FilterBuilder/utils.ts:
--------------------------------------------------------------------------------
1 | import Immutable from "immutable";
2 | import { SelectOption, ValueOption } from "../types";
3 | import { v4 as uuid } from "uuid";
4 | import { defaultLocale, rootGroupUuid } from "./constants";
5 | import { SerialisedGroup, StateTree, StateClause, TreeGroup } from "./types";
6 |
7 | import { GroupClause, ConditionClause, FilterBuilderLocaleText } from "./types"
8 |
9 | export const getDefaultCondition = (field: string): ConditionClause => ({
10 | field: field,
11 | op: "eq",
12 | value: null,
13 | id: uuid()
14 | })
15 |
16 | export const getDefaultGroup = (): GroupClause => ({
17 | connective: "and",
18 | id: uuid()
19 | });
20 |
21 | export const getSelectOption = (option: ValueOption): SelectOption => {
22 | if (typeof option === "string") {
23 | return { value: option, label: option };
24 | } else if (typeof option === "number") {
25 | return { value: option.toString(), label: option.toString() }
26 | } else {
27 | return option;
28 | }
29 | }
30 |
31 | export const getLocaleText = (key: keyof FilterBuilderLocaleText, locale: FilterBuilderLocaleText | undefined) =>
32 | locale !== undefined && locale[key] ? locale[key]! : defaultLocale[key];
33 |
34 | export const deserialise = (obj: SerialisedGroup): [StateTree, StateClause] => {
35 | const [treeGroup, clauses] = groupObjToMap(obj, rootGroupUuid);
36 |
37 | return [
38 | Immutable.Map({
39 | [rootGroupUuid]: treeGroup
40 | }),
41 | clauses
42 | ];
43 | }
44 |
45 | const groupObjToMap = (obj: SerialisedGroup, id: string, clauses?: StateClause): [TreeGroup, StateClause] => {
46 | let children = Immutable.Map();
47 |
48 | if (!clauses) {
49 | clauses = Immutable.Map();
50 | }
51 |
52 | clauses = clauses.set(id, {
53 | id: id,
54 | ...obj
55 | });
56 |
57 | obj.children.forEach((child) => {
58 | const childId = uuid();
59 | clauses = clauses!.set(childId, {
60 | id: childId,
61 | ...child
62 | });
63 |
64 | const g = child as SerialisedGroup;
65 | if (g.connective) {
66 | const result = groupObjToMap(g, childId, clauses);
67 |
68 | children = children.set(childId, result[0]);
69 | clauses = clauses.merge(result[1]);
70 | } else {
71 | children = children.set(childId, childId);
72 | }
73 | });
74 |
75 | return [{ id: id, children: children }, clauses]
76 | }
77 |
78 | export const escapeODataString = (val: string) => val.replace("'", "''");
--------------------------------------------------------------------------------
/docs/api/init-data/initdb.d/10-schema.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
2 | "MigrationId" character varying(150) NOT NULL,
3 | "ProductVersion" character varying(32) NOT NULL,
4 | CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId")
5 | );
6 |
7 | START TRANSACTION;
8 |
9 | CREATE TABLE "Customers" (
10 | "Id" integer GENERATED BY DEFAULT AS IDENTITY,
11 | "FirstName" text NOT NULL,
12 | "MiddleNames" text NULL,
13 | "Surname" text NOT NULL,
14 | "EmailAddress" text NOT NULL,
15 | "CreatedDate" timestamp with time zone NOT NULL,
16 | CONSTRAINT "PK_Customers" PRIMARY KEY ("Id")
17 | );
18 |
19 | CREATE TABLE "Product" (
20 | "Id" integer GENERATED BY DEFAULT AS IDENTITY,
21 | "Name" text NOT NULL,
22 | "Price" numeric NOT NULL,
23 | CONSTRAINT "PK_Product" PRIMARY KEY ("Id")
24 | );
25 |
26 | CREATE TABLE "Addresses" (
27 | "Id" integer GENERATED BY DEFAULT AS IDENTITY,
28 | "CustomerId" integer NOT NULL,
29 | "Line1" text NOT NULL,
30 | "Line2" text NULL,
31 | "Line3" text NULL,
32 | "Town" text NOT NULL,
33 | "County" text NULL,
34 | "Country" text NOT NULL,
35 | "PostCode" text NOT NULL,
36 | CONSTRAINT "PK_Addresses" PRIMARY KEY ("Id"),
37 | CONSTRAINT "FK_Addresses_Customers_CustomerId" FOREIGN KEY ("CustomerId") REFERENCES "Customers" ("Id") ON DELETE CASCADE
38 | );
39 |
40 | CREATE TABLE "Orders" (
41 | "Id" integer GENERATED BY DEFAULT AS IDENTITY,
42 | "CustomerId" integer NOT NULL,
43 | "DeliveryAddressId" integer NOT NULL,
44 | "Total" numeric NOT NULL,
45 | CONSTRAINT "PK_Orders" PRIMARY KEY ("Id"),
46 | CONSTRAINT "FK_Orders_Addresses_DeliveryAddressId" FOREIGN KEY ("DeliveryAddressId") REFERENCES "Addresses" ("Id") ON DELETE CASCADE,
47 | CONSTRAINT "FK_Orders_Customers_CustomerId" FOREIGN KEY ("CustomerId") REFERENCES "Customers" ("Id") ON DELETE CASCADE
48 | );
49 |
50 | CREATE TABLE "OrderProduct" (
51 | "Id" integer GENERATED BY DEFAULT AS IDENTITY,
52 | "OrderId" integer NOT NULL,
53 | "ProductId" integer NOT NULL,
54 | "Quantity" integer NOT NULL,
55 | CONSTRAINT "PK_OrderProduct" PRIMARY KEY ("Id"),
56 | CONSTRAINT "FK_OrderProduct_Orders_OrderId" FOREIGN KEY ("OrderId") REFERENCES "Orders" ("Id") ON DELETE CASCADE,
57 | CONSTRAINT "FK_OrderProduct_Product_ProductId" FOREIGN KEY ("ProductId") REFERENCES "Product" ("Id") ON DELETE CASCADE
58 | );
59 |
60 | CREATE INDEX "IX_Addresses_CustomerId" ON "Addresses" ("CustomerId");
61 |
62 | CREATE INDEX "IX_OrderProduct_OrderId" ON "OrderProduct" ("OrderId");
63 |
64 | CREATE INDEX "IX_OrderProduct_ProductId" ON "OrderProduct" ("ProductId");
65 |
66 | CREATE INDEX "IX_Orders_CustomerId" ON "Orders" ("CustomerId");
67 |
68 | CREATE INDEX "IX_Orders_DeliveryAddressId" ON "Orders" ("DeliveryAddressId");
69 |
70 | INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
71 | VALUES ('20230325165107_InitialCreate', '6.0.4');
72 |
73 | COMMIT;
74 |
--------------------------------------------------------------------------------
/packages/base/types.ts:
--------------------------------------------------------------------------------
1 |
2 | import { ResponsiveValues } from "./hooks"
3 | import { ExternalBuilderProps, FieldDef } from "./FilterBuilder/types"
4 | import React from "react";
5 |
6 | export type ODataGridBaseProps<
7 | ComponentProps extends IGridProps,
8 | SortModel extends IGridSortModel,
9 | ColDef,
10 | TRow,
11 | TDate
12 | > =
13 | OmitGridProps
14 | &
15 | {
16 | url: string,
17 | alwaysSelect?: string[],
18 | columns: ODataGridBaseColDef[],
19 | columnVisibilityModel?: ODataColumnVisibilityModel,
20 | component: React.ElementType,
21 | defaultPageSize?: number,
22 | defaultSortModel?: SortModel,
23 | disableFilterBuilder?: boolean,
24 | disableHistory?: boolean,
25 | $filter?: string,
26 | filterBuilderProps?: ExternalBuilderProps,
27 | requestOptions?: RequestInit
28 | };
29 |
30 | // remove properties which should not be used - these are handled internally or overridden
31 | type OmitGridProps = Omit
50 |
51 | type ODataColumn = Omit & FieldDef & {
52 | select?: string,
53 | expand?: Expand | Expand[],
54 | hide?: ResponsiveValues | boolean,
55 | filterOnly?: boolean
56 | }
57 |
58 | // type for rows when displayed in datagrid
59 | // allows object to be flattened for convenience, but still allows strong typing through "result" property
60 | export type ODataRowModel = {
61 | result: T,
62 | [key: string]: any
63 | }
64 |
65 | export type ODataGridBaseColDef = ODataColumn
66 | export type ODataGridBaseEnrichedColDef =
67 | | ODataColumn
68 | | ODataColumn;
69 |
70 | export type ODataBaseGridColumns = ODataGridBaseEnrichedColDef[]
71 |
72 | export type ODataResponse = {
73 | "@odata.count"?: number,
74 | value: T[]
75 | }
76 |
77 | export type Expand = {
78 | navigationField: string,
79 | select?: string,
80 | expand?: Expand[] | Expand,
81 | orderBy?: string,
82 | top?: number,
83 | count?: boolean
84 | }
85 |
86 | export type ValueOption = string | number | SelectOption;
87 |
88 | export type SelectOption = {
89 | value: any,
90 | label: string
91 | }
92 |
93 | export type ODataColumnVisibilityModel = Record>;
94 |
95 | export type ColumnVisibilityModel = Record;
96 |
97 | export type IGridSortModel = ({ field: string, sort: 'asc' | 'desc' | null | undefined })[];
98 |
99 | export type IGridRowModel = T;
100 |
101 | export type IGridProps = {
102 | onColumnVisibilityModelChange?: any,
103 | columnVisibilityModel?: ColumnVisibilityModel,
104 | onSortModelChange?: any
105 | }
--------------------------------------------------------------------------------
/docs/api/Data/Seeder.cs:
--------------------------------------------------------------------------------
1 | using Api.Models;
2 | using Bogus;
3 | using Microsoft.EntityFrameworkCore;
4 |
5 | namespace Api.Data;
6 |
7 | public interface ISeeder
8 | {
9 | Task Seed(CancellationToken token);
10 | }
11 |
12 | public class Seeder : ISeeder
13 | {
14 | private readonly ApiContext _context;
15 |
16 | public Seeder(ApiContext context)
17 | {
18 | _context = context;
19 | }
20 |
21 | public async Task Seed(CancellationToken token)
22 | {
23 | if (await _context.Customers.AnyAsync(token))
24 | {
25 | return;
26 | }
27 |
28 | var productFaker = new Faker()
29 | .RuleFor(p => p.Name, f => f.Commerce.ProductName())
30 | .RuleFor(p => p.Price, f => Math.Round(f.Random.Decimal(0, 500), 2))
31 | .FinishWith((_, _) => token.ThrowIfCancellationRequested());
32 |
33 | List products = productFaker.GenerateBetween(100, 200);
34 |
35 | int line1Index = 0;
36 | var addressFaker = new Faker()
37 | .RuleFor(a => a.Line1, f => f.PickRandomParam(out line1Index, f.Address.StreetAddress(), f.Address.SecondaryAddress()))
38 | .RuleFor(a => a.Line2, f => line1Index == 1 ? f.Address.StreetAddress() : null)
39 | .RuleFor(a => a.Line3, f => f.Address.City().OrNull(f, .9f))
40 | .RuleFor(a => a.Town, f => f.Address.City())
41 | .RuleFor(a => a.Country, f => f.Address.Country())
42 | .RuleFor(a => a.County, (f, a) => a.Country == "United Kingdom"
43 | ? f.Address.County().OrNull(f)
44 | : null)
45 | .RuleFor(a => a.PostCode, f => f.Address.ZipCode())
46 | .FinishWith((_, _) => token.ThrowIfCancellationRequested());
47 |
48 | var orderProductFaker = new Faker()
49 | .RuleFor(op => op.Product, f => f.PickRandom(products))
50 | .RuleFor(op => op.Quantity, f => f.Random.Int(1, 5))
51 | .FinishWith((_, _) => token.ThrowIfCancellationRequested());
52 |
53 | var orderFaker = new Faker()
54 | .RuleFor(o => o.Date, f => f.Date.PastOffset(5).ToUniversalTime())
55 | .RuleFor(o => o.OrderProducts, _ => orderProductFaker.GenerateBetween(1, 5))
56 | .FinishWith((_, o) =>
57 | {
58 | token.ThrowIfCancellationRequested();
59 | o.Total = o.OrderProducts.Sum(op => op.Product.Price * op.Quantity);
60 | });
61 |
62 | var customerFaker = new Faker()
63 | .RuleFor(c => c.FirstName, f => f.Name.FirstName())
64 | .RuleFor(c => c.MiddleNames, f => f.Name.FirstName().OrNull(f))
65 | .RuleFor(c => c.Surname, f => f.Name.LastName())
66 | .RuleFor(c => c.EmailAddress, (f, c) => f.Internet.ExampleEmail(c.FirstName, c.Surname))
67 | .RuleFor(c => c.Addresses, _ => addressFaker.GenerateBetween(1, 3))
68 | .RuleFor(c => c.Orders, _ => orderFaker.GenerateBetween(0, 10))
69 | .FinishWith((f, c) =>
70 | {
71 | token.ThrowIfCancellationRequested();
72 | foreach (var order in c.Orders)
73 | {
74 | order.DeliveryAddress = f.PickRandom(c.Addresses);
75 | }
76 |
77 | c.CreatedDate = c.Orders.Select(o => o.Date).DefaultIfEmpty(f.Date.PastOffset(5).ToUniversalTime()).Min();
78 | });
79 |
80 | List customers = customerFaker.GenerateBetween(800, 1200);
81 |
82 | _context.Customers.AddRange(customers);
83 | await _context.SaveChangesAsync(token);
84 | }
85 | }
--------------------------------------------------------------------------------
/packages/o-data-grid/pnpm-lock.yaml:
--------------------------------------------------------------------------------
1 | lockfileVersion: 5.3
2 |
3 | specifiers:
4 | immutable: ^4.0.0
5 | recoil: 0.7.2
6 | tss-react: 3.6.2
7 | uuid: ^8.3.2
8 |
9 | dependencies:
10 | immutable: 4.0.0
11 | recoil: 0.7.2
12 | tss-react: 3.6.2
13 | uuid: 8.3.2
14 |
15 | packages:
16 |
17 | /@emotion/cache/11.7.1:
18 | resolution: {integrity: sha512-r65Zy4Iljb8oyjtLeCuBH8Qjiy107dOYC6SJq7g7GV5UCQWMObY4SJDPGFjiiVpPrOJ2hmJOoBiYTC7hwx9E2A==}
19 | dependencies:
20 | '@emotion/memoize': 0.7.5
21 | '@emotion/sheet': 1.1.0
22 | '@emotion/utils': 1.0.0
23 | '@emotion/weak-memoize': 0.2.5
24 | stylis: 4.0.13
25 | dev: false
26 |
27 | /@emotion/hash/0.8.0:
28 | resolution: {integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==}
29 | dev: false
30 |
31 | /@emotion/memoize/0.7.5:
32 | resolution: {integrity: sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ==}
33 | dev: false
34 |
35 | /@emotion/serialize/1.0.2:
36 | resolution: {integrity: sha512-95MgNJ9+/ajxU7QIAruiOAdYNjxZX7G2mhgrtDWswA21VviYIRP1R5QilZ/bDY42xiKsaktP4egJb3QdYQZi1A==}
37 | dependencies:
38 | '@emotion/hash': 0.8.0
39 | '@emotion/memoize': 0.7.5
40 | '@emotion/unitless': 0.7.5
41 | '@emotion/utils': 1.0.0
42 | csstype: 3.0.10
43 | dev: false
44 |
45 | /@emotion/sheet/1.1.0:
46 | resolution: {integrity: sha512-u0AX4aSo25sMAygCuQTzS+HsImZFuS8llY8O7b9MDRzbJM0kVJlAz6KNDqcG7pOuQZJmj/8X/rAW+66kMnMW+g==}
47 | dev: false
48 |
49 | /@emotion/unitless/0.7.5:
50 | resolution: {integrity: sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==}
51 | dev: false
52 |
53 | /@emotion/utils/1.0.0:
54 | resolution: {integrity: sha512-mQC2b3XLDs6QCW+pDQDiyO/EdGZYOygE8s5N5rrzjSI4M3IejPE/JPndCBwRT9z982aqQNi6beWs1UeayrQxxA==}
55 | dev: false
56 |
57 | /@emotion/weak-memoize/0.2.5:
58 | resolution: {integrity: sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==}
59 | dev: false
60 |
61 | /csstype/3.0.10:
62 | resolution: {integrity: sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==}
63 | dev: false
64 |
65 | /hamt_plus/1.0.2:
66 | resolution: {integrity: sha1-4hwlKWjH4zsg9qGwlM2FeHomVgE=}
67 | dev: false
68 |
69 | /immutable/4.0.0:
70 | resolution: {integrity: sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==}
71 | dev: false
72 |
73 | /recoil/0.7.2:
74 | resolution: {integrity: sha512-OT4pI7FOUHcIoRtjsL5Lqq+lFFzQfir4MIbUkqyJ3nqv3WfBP1pHepyurqTsK5gw+T+I2R8+uOD28yH+Lg5o4g==}
75 | peerDependencies:
76 | react: '>=16.13.1'
77 | react-dom: '*'
78 | react-native: '*'
79 | peerDependenciesMeta:
80 | react-dom:
81 | optional: true
82 | react-native:
83 | optional: true
84 | dependencies:
85 | hamt_plus: 1.0.2
86 | dev: false
87 |
88 | /stylis/4.0.13:
89 | resolution: {integrity: sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag==}
90 | dev: false
91 |
92 | /tss-react/3.6.2:
93 | resolution: {integrity: sha512-+ecQIqCNFVlVJVk3NiCWZY2+5DhVKMVUVxIbEwv9nYsTRQA/KxyKCU8g+KMj5ByqFv9LW76+TUYzbWck/IHkfA==}
94 | peerDependencies:
95 | '@emotion/react': ^11.4.1
96 | '@emotion/server': ^11.4.0
97 | react: ^16.8.0 || ^17.0.2 || ^18.0.0
98 | peerDependenciesMeta:
99 | '@emotion/server':
100 | optional: true
101 | dependencies:
102 | '@emotion/cache': 11.7.1
103 | '@emotion/serialize': 1.0.2
104 | '@emotion/utils': 1.0.0
105 | dev: false
106 |
107 | /uuid/8.3.2:
108 | resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
109 | hasBin: true
110 | dev: false
111 |
--------------------------------------------------------------------------------
/packages/o-data-grid-pro/pnpm-lock.yaml:
--------------------------------------------------------------------------------
1 | lockfileVersion: 5.3
2 |
3 | specifiers:
4 | immutable: ^4.0.0
5 | recoil: 0.7.2
6 | tss-react: 3.6.2
7 | uuid: ^8.3.2
8 |
9 | dependencies:
10 | immutable: 4.0.0
11 | recoil: 0.7.2
12 | tss-react: 3.6.2
13 | uuid: 8.3.2
14 |
15 | packages:
16 |
17 | /@emotion/cache/11.7.1:
18 | resolution: {integrity: sha512-r65Zy4Iljb8oyjtLeCuBH8Qjiy107dOYC6SJq7g7GV5UCQWMObY4SJDPGFjiiVpPrOJ2hmJOoBiYTC7hwx9E2A==}
19 | dependencies:
20 | '@emotion/memoize': 0.7.5
21 | '@emotion/sheet': 1.1.0
22 | '@emotion/utils': 1.0.0
23 | '@emotion/weak-memoize': 0.2.5
24 | stylis: 4.0.13
25 | dev: false
26 |
27 | /@emotion/hash/0.8.0:
28 | resolution: {integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==}
29 | dev: false
30 |
31 | /@emotion/memoize/0.7.5:
32 | resolution: {integrity: sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ==}
33 | dev: false
34 |
35 | /@emotion/serialize/1.0.2:
36 | resolution: {integrity: sha512-95MgNJ9+/ajxU7QIAruiOAdYNjxZX7G2mhgrtDWswA21VviYIRP1R5QilZ/bDY42xiKsaktP4egJb3QdYQZi1A==}
37 | dependencies:
38 | '@emotion/hash': 0.8.0
39 | '@emotion/memoize': 0.7.5
40 | '@emotion/unitless': 0.7.5
41 | '@emotion/utils': 1.0.0
42 | csstype: 3.0.10
43 | dev: false
44 |
45 | /@emotion/sheet/1.1.0:
46 | resolution: {integrity: sha512-u0AX4aSo25sMAygCuQTzS+HsImZFuS8llY8O7b9MDRzbJM0kVJlAz6KNDqcG7pOuQZJmj/8X/rAW+66kMnMW+g==}
47 | dev: false
48 |
49 | /@emotion/unitless/0.7.5:
50 | resolution: {integrity: sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==}
51 | dev: false
52 |
53 | /@emotion/utils/1.0.0:
54 | resolution: {integrity: sha512-mQC2b3XLDs6QCW+pDQDiyO/EdGZYOygE8s5N5rrzjSI4M3IejPE/JPndCBwRT9z982aqQNi6beWs1UeayrQxxA==}
55 | dev: false
56 |
57 | /@emotion/weak-memoize/0.2.5:
58 | resolution: {integrity: sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==}
59 | dev: false
60 |
61 | /csstype/3.0.10:
62 | resolution: {integrity: sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==}
63 | dev: false
64 |
65 | /hamt_plus/1.0.2:
66 | resolution: {integrity: sha1-4hwlKWjH4zsg9qGwlM2FeHomVgE=}
67 | dev: false
68 |
69 | /immutable/4.0.0:
70 | resolution: {integrity: sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==}
71 | dev: false
72 |
73 | /recoil/0.7.2:
74 | resolution: {integrity: sha512-OT4pI7FOUHcIoRtjsL5Lqq+lFFzQfir4MIbUkqyJ3nqv3WfBP1pHepyurqTsK5gw+T+I2R8+uOD28yH+Lg5o4g==}
75 | peerDependencies:
76 | react: '>=16.13.1'
77 | react-dom: '*'
78 | react-native: '*'
79 | peerDependenciesMeta:
80 | react-dom:
81 | optional: true
82 | react-native:
83 | optional: true
84 | dependencies:
85 | hamt_plus: 1.0.2
86 | dev: false
87 |
88 | /stylis/4.0.13:
89 | resolution: {integrity: sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag==}
90 | dev: false
91 |
92 | /tss-react/3.6.2:
93 | resolution: {integrity: sha512-+ecQIqCNFVlVJVk3NiCWZY2+5DhVKMVUVxIbEwv9nYsTRQA/KxyKCU8g+KMj5ByqFv9LW76+TUYzbWck/IHkfA==}
94 | peerDependencies:
95 | '@emotion/react': ^11.4.1
96 | '@emotion/server': ^11.4.0
97 | react: ^16.8.0 || ^17.0.2 || ^18.0.0
98 | peerDependenciesMeta:
99 | '@emotion/server':
100 | optional: true
101 | dependencies:
102 | '@emotion/cache': 11.7.1
103 | '@emotion/serialize': 1.0.2
104 | '@emotion/utils': 1.0.0
105 | dev: false
106 |
107 | /uuid/8.3.2:
108 | resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
109 | hasBin: true
110 | dev: false
111 |
--------------------------------------------------------------------------------
/packages/base/utils.ts:
--------------------------------------------------------------------------------
1 | import { Expand } from "./types";
2 |
3 | import { defaultPageSize } from "./constants";
4 |
5 | /**
6 | * Convert an Expand object (or array of objects) to a clause to use in an OData $expand query parameter
7 | * @param e Expand(s) to convert
8 | * @returns OData expand clause string
9 | */
10 | export const ExpandToQuery = (expand?: Expand[] | Expand): string => {
11 | if (expand === undefined) {
12 | return "";
13 | }
14 |
15 | if (!Array.isArray(expand)) {
16 | return ExpandToQuery([expand]);
17 | }
18 |
19 | // group all expands by the navigation field
20 | const groupedExpands = GroupArrayBy(expand, (e) => e.navigationField);
21 |
22 | // construct a single expand for each navigation field, combining nested query options (where possible)
23 | const expands: Expand[] = [];
24 | groupedExpands.forEach((e, k) => {
25 | expands.push({
26 | navigationField: k,
27 | top: e.find(e2 => e2.top)?.top,
28 | orderBy: e.find(e2 => e2.orderBy)?.orderBy,
29 | count: e.some(e2 => e2.count),
30 | select: Array.from(new Set(e.filter(e2 => e2.select).map(e2 => e2.select))).join(","),
31 | expand: e.filter(e2 => e2.expand)
32 | .map(e2 => e2.expand!)
33 | .reduce((a: Expand[], b) => Array.isArray(b) ? a.concat(b) : [...a, b], [])
34 | });
35 | });
36 |
37 | return expands.map(e => {
38 | let result = `${e.navigationField}`;
39 |
40 | const options = [
41 | { type: "select", value: e.select },
42 | { type: "expand", value: ExpandToQuery(e.expand) },
43 | { type: "orderby", value: e.orderBy },
44 | { type: "top", value: e.top },
45 | { type: "count", value: e.count }
46 | ];
47 |
48 | if (options.some(o => o.value)) {
49 | result += `(${options.filter(o => o.value).map(o => `$${o.type}=${o.value}`).join(";")})`
50 | }
51 |
52 | return result;
53 |
54 | }).join(",")
55 | }
56 |
57 | /**
58 | * Group an array into multiple arrays linked by a common key value
59 | * @param arr Array to group
60 | * @param keySelector Function to select property to group by
61 | * @returns ES6 Map of keys to arrays of values
62 | */
63 | export const GroupArrayBy = (arr: T[], keySelector: (e: T) => TKey) => arr
64 | .reduce((m, e) => m.set(keySelector(e), [...m.get(keySelector(e)) || [], e]), new Map());
65 |
66 | /**
67 | * Flatten an object to a single level, i.e. { Person: { Name: "John" } } becomes { "Person.Name": "John" }.
68 | * Arrays are kept as arrays, with their elements flattened.
69 | * @param obj Object to flatten
70 | * @param sep Level separator (default ".")
71 | * @returns Flattened object
72 | */
73 |
74 | export const Flatten = (obj: any, sep = ".") => _flatten(obj, sep, "");
75 |
76 | const _flatten = (obj: any, sep: string, prefix: string) =>
77 | Object.keys(obj).reduce((x: { [key: string]: any }, k) => {
78 | if (obj[k] !== null) {
79 | const pre = prefix.length ? prefix + sep : "";
80 | if (Array.isArray(obj[k])) {
81 | x[pre + k] = (obj[k] as Array).map(i => Flatten(i, sep));
82 | } else if (typeof obj[k] === "object") {
83 | Object.assign(x, _flatten(obj[k], sep, pre + k));
84 | } else {
85 | x[pre + k] = obj[k];
86 | }
87 | }
88 | return x;
89 | }, {});
90 |
91 | export const GetPageNumber = () => {
92 | const params = new URLSearchParams(window.location.search);
93 | if (params.has("page")) {
94 | const pageVal = params.get("page");
95 | if (pageVal) {
96 | return parseInt(pageVal, 10) - 1;
97 | }
98 | }
99 |
100 | return 0;
101 | }
102 |
103 | export const GetPageSizeOrDefault = (defaultSize?: number) => {
104 | const params = new URLSearchParams(window.location.search);
105 | if (params.has("page-size")) {
106 | const sizeVal = params.get("page-size");
107 | if (sizeVal) {
108 | return parseInt(sizeVal, 10);
109 | }
110 | }
111 |
112 | return defaultSize ?? defaultPageSize;
113 | }
--------------------------------------------------------------------------------
/packages/base/FilterBuilder/types.ts:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { DatePickerProps, DateTimePickerProps, LocalizationProviderProps } from "@mui/x-date-pickers";
3 | import { AutocompleteProps, FormControlProps, SelectProps, TextFieldProps } from "@mui/material";
4 | import { GridValueOptionsParams } from "@mui/x-data-grid";
5 | import { ValueOption } from "../types";
6 |
7 | export type ExternalBuilderProps = {
8 | searchMenuItems?: ({ label: string, onClick: () => void })[],
9 | onSubmit?: (params: FilterParameters) => (void | any),
10 | onRestoreState?: (params: FilterParameters, state?: any) => void,
11 | localeText?: FilterBuilderLocaleText,
12 |
13 | autocompleteGroups?: string[],
14 |
15 | autocompleteProps?: AutocompleteProps,
16 | datePickerProps?: DatePickerProps,
17 | dateTimePickerProps?: DateTimePickerProps,
18 | localizationProviderProps?: LocalizationProviderProps,
19 | selectProps?: SelectProps,
20 | textFieldProps?: TextFieldProps,
21 |
22 | disableHistory?: boolean,
23 |
24 | filter?: SerialisedGroup
25 | }
26 |
27 | export type FilterParameters = {
28 | compute?: string,
29 | filter: string,
30 | queryString?: QueryStringCollection,
31 | select?: string[],
32 | serialised?: SerialisedGroup,
33 | }
34 |
35 | export type FilterBuilderLocaleText = {
36 | and?: string,
37 | or?: string,
38 |
39 | addCondition?: string,
40 | addGroup?: string,
41 |
42 | field?: string,
43 | operation?: string,
44 | value?: string,
45 | collectionOperation?: string,
46 | collectionField?: string,
47 |
48 | search?: string,
49 | reset?: string
50 |
51 | opAny?: string,
52 | opAll?: string,
53 | opCount?: string,
54 |
55 | opEq?: string,
56 | opNe?: string,
57 | opGt?: string,
58 | opLt?: string,
59 | opGe?: string,
60 | opLe?: string,
61 | opContains?: string,
62 | opNull?: string,
63 | opNotNull?: string
64 | }
65 |
66 | export type BaseFieldDef = {
67 | field: string,
68 | autocompleteGroup?: string,
69 | caseSensitive?: boolean,
70 |
71 | datePickerProps?: DatePickerProps,
72 | dateTimePickerProps?: DateTimePickerProps,
73 |
74 | filterable?: boolean,
75 | filterField?: string,
76 | filterOperators?: Operation[],
77 | filterType?: string,
78 |
79 | getCustomFilterString?: (op: Operation, value: any) => string | FilterCompute | boolean,
80 | getCustomQueryString?: (op: Operation, value: any) => QueryStringCollection,
81 |
82 | label?: string,
83 | nullable?: boolean,
84 |
85 | selectProps?: { selectProps?: SelectProps, formControlProps?: FormControlProps, label?: string },
86 | sortField?: string,
87 | textFieldProps?: TextFieldProps,
88 |
89 | renderCustomInput?: (value: any, setValue: (v: any) => void) => React.ReactNode,
90 | renderCustomFilter?: (value: any, setValue: (v: any) => void) => React.ReactNode,
91 |
92 | type?: string,
93 | valueOptions?: ValueOption[] | ((params: GridValueOptionsParams) => ValueOption[]),
94 | }
95 |
96 | export type FieldDef = BaseFieldDef & {
97 | headerName?: string,
98 | collection?: boolean,
99 | collectionFields?: CollectionFieldDef[],
100 | }
101 |
102 | export type CollectionFieldDef = BaseFieldDef;
103 |
104 | export type FilterCompute = {
105 | filter: string,
106 | compute: string | ComputeSelect
107 | }
108 |
109 | export type ComputeSelect = {
110 | compute: string,
111 | select: string[]
112 | }
113 |
114 | export type QueryStringCollection = {
115 | [key: string]: string
116 | }
117 |
118 | export type FilterTranslatorCollection = {
119 | [key in Operation | "default"]?: FilterTranslator
120 | }
121 |
122 | export type FilterTranslator = (params: FilterTranslatorParams) => string | boolean;
123 |
124 | export type FilterTranslatorParams = {
125 | schema: BaseFieldDef,
126 | field: string,
127 | op: Operation,
128 | value: any
129 | }
130 |
131 | export type Connective = "and" | "or"
132 |
133 | export type Operation = "eq" | "ne" | "gt" | "lt" | "ge" | "le" | "contains" | "null" | "notnull"
134 |
135 | export type CollectionOperation = "any" | "all" | "count"
136 |
137 | type Clause = {
138 | id: string
139 | }
140 |
141 | export type GroupClause = Clause & {
142 | connective: Connective
143 | }
144 |
145 | export type ConditionClause = Clause & {
146 | field: string,
147 | op: Operation;
148 | collectionOp?: CollectionOperation,
149 | collectionField?: string,
150 | value: any,
151 | default?: boolean
152 | }
153 |
154 | export type TreeGroup = Clause & {
155 | children: TreeChildren
156 | }
157 |
158 | export type TreeChildren = Immutable.Map;
159 |
160 | export type StateClause = Immutable.Map;
161 | export type StateTree = Immutable.Map;
162 |
163 | export type SerialisedGroup = Omit & {
164 | children: (SerialisedGroup | SerialisedCondition)[]
165 | }
166 |
167 | export type SerialisedCondition = Omit;
--------------------------------------------------------------------------------
/packages/base/FilterBuilder/hooks.ts:
--------------------------------------------------------------------------------
1 | import { useCallback } from "react";
2 | import { useRecoilValue, waitForAll } from "recoil"
3 | import { rootGroupUuid } from "./constants";
4 | import { clauseState, schemaState, treeState } from "./state"
5 | import { defaultTranslators } from "./translation";
6 | import { BaseFieldDef, SerialisedCondition, ConditionClause, FieldDef, SerialisedGroup, GroupClause, Operation, QueryStringCollection, StateClause, StateTree, TreeGroup, FilterTranslator } from "./types";
7 |
8 | export const UseODataFilter = () => {
9 | const schema = useRecoilValue(schemaState);
10 | const [clauses, tree] = useRecoilValue(waitForAll([clauseState, treeState]));
11 |
12 | return useCallback(() => {
13 | return buildGroup(schema, clauses, tree, rootGroupUuid, []) as BuiltQuery;
14 | }, [schema, clauses, tree]);
15 | }
16 |
17 | export const UseODataFilterWithState = () => {
18 | const schema = useRecoilValue(schemaState);
19 |
20 | return useCallback((clauses: StateClause, tree: StateTree) => {
21 | return buildGroup(schema, clauses, tree, rootGroupUuid, []) as BuiltQuery;
22 | }, [schema])
23 | }
24 |
25 | type BuiltInnerQuery = {
26 | filter?: string,
27 | compute?: string,
28 | select?: string[],
29 | queryString?: QueryStringCollection
30 | }
31 |
32 | type BuiltQuery = BuiltInnerQuery & {
33 | serialised: T
34 | }
35 |
36 | const buildGroup = (schema: FieldDef[], clauses: StateClause, tree: StateTree, id: string, path: string[]): (BuiltQuery | boolean) => {
37 | const clause = clauses.get(id) as GroupClause;
38 | const treeNode = tree.getIn([...path, id]) as TreeGroup;
39 |
40 | if (!treeNode) {
41 | console.error(`Tree node ${[...path, id].join("->")} not found`);
42 | return false;
43 | }
44 |
45 | const childClauses = treeNode.children
46 | .toArray()
47 | .map((c) => {
48 | if (typeof c[1] === "string") {
49 | return buildCondition(schema, clauses, c[0]);
50 | } else {
51 | return buildGroup(schema, clauses, tree, c[0], [...path, id, "children"]);
52 | }
53 | })
54 | .filter(c => c !== false) as (BuiltQuery | BuiltQuery)[];
55 |
56 | if (childClauses.length > 1) {
57 | return {
58 | filter: `(${childClauses.filter(c => c.filter).map(c => c.filter).join(` ${clause.connective} `)})`,
59 | compute: `${childClauses.filter(c => c.compute).map(c => c.compute).join(",")}`,
60 | select: childClauses.filter(c => c.select).flatMap(c => c.select!),
61 | serialised: { connective: clause.connective, children: childClauses.map(c => c.serialised) },
62 | queryString: childClauses.reduce((x, c) => ({ ...x, ...c.queryString }), {})
63 | };
64 | } else if (childClauses.length === 1) {
65 | return {
66 | filter: childClauses[0].filter,
67 | compute: childClauses[0].compute,
68 | select: childClauses[0].select,
69 | serialised: { connective: clause.connective, children: [childClauses[0].serialised] },
70 | queryString: childClauses[0].queryString
71 | }
72 | } else {
73 | console.error("Group has no children");
74 | return false;
75 | }
76 | }
77 |
78 | const buildCondition = (schema: FieldDef[], clauses: StateClause, id: string): (BuiltQuery | boolean) => {
79 | const clause = clauses.get(id) as ConditionClause;
80 |
81 | let condition: SerialisedCondition | undefined = undefined;
82 | if (!clause || clause.default === true) {
83 | console.error(`Clause not found: ${id}`);
84 | return false;
85 | } else {
86 | condition = {
87 | field: clause.field,
88 | op: clause.op,
89 | collectionOp: clause.collectionOp,
90 | collectionField: clause.collectionField,
91 | value: clause.value
92 | }
93 | }
94 |
95 | const def = schema.find(d => d.field === clause.field);
96 |
97 | if (!def) {
98 | console.error(`Schema entry not found for field "${clause.field}"`);
99 | return false;
100 | }
101 |
102 | const filterField = def.filterField ?? def.field;
103 |
104 | let innerResult;
105 | if (clause.collectionOp) {
106 | if (clause.collectionOp === "count") {
107 | innerResult = {
108 | filter: `${filterField}/$count ${clause.op} ${clause.value}`
109 | };
110 | } else {
111 | const collectionDef = def.collectionFields!.find(d => d.field === clause.collectionField!);
112 | innerResult = buildInnerCondition(collectionDef!, "x/" + clause.collectionField!, clause.op, clause.value);
113 | }
114 | } else {
115 | innerResult = buildInnerCondition(def, filterField, clause.op, clause.value);
116 | }
117 |
118 | if (typeof innerResult !== "boolean") {
119 | if (innerResult.filter) {
120 | return {
121 | filter: innerResult.filter,
122 | compute: innerResult.compute,
123 | select: innerResult.select,
124 | serialised: condition
125 | }
126 | } else {
127 | return {
128 | serialised: condition,
129 | queryString: innerResult.queryString
130 | };
131 | }
132 | } else {
133 | return false;
134 | }
135 | }
136 |
137 | const buildInnerCondition = (schema: BaseFieldDef, field: string, op: Operation, value: any): BuiltInnerQuery | boolean => {
138 | if (schema.getCustomQueryString) {
139 | return {
140 | queryString: schema.getCustomQueryString(op, value)
141 | };
142 | }
143 |
144 | if (schema.getCustomFilterString) {
145 | const result = schema.getCustomFilterString(op, value);
146 |
147 | if (typeof result === "string") {
148 | return {
149 | filter: result
150 | }
151 | } else if (typeof result !== "boolean") {
152 | const compute = result.compute;
153 | if (typeof compute === "string") {
154 | return {
155 | filter: result.filter,
156 | compute: compute
157 | };
158 | } else {
159 | return {
160 | filter: result.filter,
161 | compute: compute.compute,
162 | select: compute.select
163 | };
164 | }
165 | } else {
166 | return result;
167 | }
168 | }
169 |
170 | let translator: FilterTranslator;
171 | if (op in defaultTranslators) {
172 | translator = defaultTranslators[op]!;
173 | } else {
174 | translator = defaultTranslators["default"]!;
175 | }
176 |
177 | const result = translator({ schema, field, op, value });
178 |
179 | if (typeof result === "string") {
180 | return {
181 | filter: result
182 | };
183 | } else {
184 | return result;
185 | }
186 | }
--------------------------------------------------------------------------------
/packages/base/FilterBuilder/components/FilterGroup.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useMemo } from "react"
2 | import { useRecoilValue, useSetRecoilState, waitForAll } from "recoil";
3 | import Immutable from "immutable";
4 | import { Button, ButtonGroup, Grid, ToggleButton, ToggleButtonGroup } from "@mui/material";
5 | import { Add } from "@mui/icons-material";
6 |
7 | import FilterCondition from "./FilterCondition";
8 |
9 | import { Connective, GroupClause, TreeChildren, TreeGroup } from "../types"
10 |
11 | import { clauseState, propsState, schemaState, treeState } from "../state"
12 |
13 | import { getDefaultCondition, getDefaultGroup, getLocaleText } from "../utils";
14 |
15 | import makeStyles from "../../makeStyles";
16 | import { useResponsive } from "../../hooks";
17 |
18 |
19 | const useStyles = makeStyles()((theme) => ({
20 | group: {
21 | borderWidth: 1,
22 | borderColor: theme.palette.mode === "dark" ? "rgb(81,81,81)" : "rgb(224,224,224)",
23 | borderRadius: theme.shape.borderRadius,
24 | borderStyle: "solid",
25 | padding: theme.spacing(2),
26 | },
27 | child: {
28 | position: "relative",
29 | "&:not(:last-of-type)::before": {
30 | content: "''",
31 | display: "block",
32 | position: "absolute",
33 | width: 2,
34 | height: "100%",
35 | background: theme.palette.primary.main,
36 | left: theme.spacing(-1),
37 | },
38 | "&:first-of-type::before": {
39 | height: `calc(100% + ${theme.spacing(2)})`,
40 | top: 0
41 | },
42 | "&::after": {
43 | content: "''",
44 | display: "block",
45 | position: "absolute",
46 | left: theme.spacing(-1),
47 | top: `calc(${theme.spacing(1)} + 1px)`,
48 | width: theme.spacing(2),
49 | height: "50%",
50 | borderWidth: 2,
51 | borderStyle: "solid",
52 | borderColor: theme.palette.primary.main,
53 | borderRight: "none",
54 | borderTop: "none",
55 | borderBottomLeftRadius: theme.shape.borderRadius
56 | }
57 | }
58 | }));
59 |
60 |
61 | type FilterGroupProps = {
62 | clauseId: string,
63 | path: string[],
64 | root?: boolean
65 | }
66 |
67 | const FilterGroup = ({ clauseId, path, root }: FilterGroupProps) => {
68 | const { classes } = useStyles();
69 | const r = useResponsive();
70 |
71 | const [tree, clauses] = useRecoilValue(waitForAll([treeState, clauseState]));
72 | const setTree = useSetRecoilState(treeState);
73 | const setClauses = useSetRecoilState(clauseState);
74 | const schema = useRecoilValue(schemaState);
75 | const builderProps = useRecoilValue(propsState);
76 |
77 | const group = useMemo(() => clauses.get(clauseId) as GroupClause, [clauses, clauseId]);
78 | const treeGroup = useMemo(() => tree.getIn([...path, clauseId]) as TreeGroup, [tree, path, clauseId]);
79 |
80 | const childrenPath = useMemo(() => [...path, clauseId, "children"], [path, clauseId]);
81 |
82 | const multiple = useMemo(() => treeGroup.children.count() > 1, [treeGroup]);
83 |
84 | const setConnective = useCallback((con: Connective) => {
85 | setClauses(clauses.update(clauseId, c => ({...c as GroupClause, connective: con})))
86 | }, [clauses, setClauses, clauseId]);
87 |
88 | const addGroup = useCallback(() => {
89 | const group = getDefaultGroup();
90 | const condition = getDefaultCondition(schema[0].field);
91 |
92 | setClauses(clauses
93 | .set(group.id, group)
94 | .set(condition.id, condition)
95 | );
96 |
97 | setTree(tree
98 | .updateIn(
99 | childrenPath,
100 | (list) => (list as TreeChildren).set(group.id, { id: group.id, children: Immutable.Map({ [condition.id]: condition.id }) })
101 | )
102 | );
103 | }, [clauses, setClauses, tree, setTree, childrenPath, schema]);
104 |
105 | const addCondition = useCallback(() => {
106 | const condition = getDefaultCondition(schema[0].field);
107 |
108 | setClauses(clauses.set(condition.id, condition));
109 |
110 | setTree(tree
111 | .updateIn(
112 | childrenPath,
113 | (list) => (list as TreeChildren).set(condition.id, condition.id)
114 | )
115 | );
116 | }, [clauses, setClauses, tree, setTree, childrenPath, schema]);
117 |
118 | const handleConnective = useCallback((event, val: Connective | null) => {
119 | if (val) {
120 | setConnective(val);
121 | }
122 | }, [setConnective]);
123 |
124 | return (
125 |
126 |
127 | {multiple && (
128 |
129 |
137 | {getLocaleText("and", builderProps.localeText)}
138 | {getLocaleText("or", builderProps.localeText)}
139 |
140 |
141 | )}
142 |
143 |
144 | } onClick={addCondition}>{getLocaleText("addCondition", builderProps.localeText)}
145 | } onClick={addGroup}>{getLocaleText("addGroup", builderProps.localeText)}
146 |
147 |
148 |
149 |
150 | {treeGroup.children.toArray().map((c) => {
151 | if (typeof c[1] === "string") {
152 | return (
153 |
154 |
158 |
159 | );
160 | } else {
161 | return (
162 |
163 |
167 |
168 | );
169 | }
170 | })}
171 |
172 |
173 | )
174 | }
175 |
176 | export default FilterGroup;
--------------------------------------------------------------------------------
/packages/base/FilterBuilder/components/FilterCondition.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useMemo } from "react"
2 | import { useRecoilState, useRecoilValue, useSetRecoilState } from "recoil";
3 | import { Grid, IconButton } from "@mui/material";
4 | import { Remove } from "@mui/icons-material";
5 |
6 | import FilterInputs from "./FilterInputs";
7 |
8 | import { CollectionOperation, ConditionClause, Operation, TreeGroup } from "../types"
9 |
10 | import { clauseState, schemaState, treeState } from "../state"
11 |
12 | import { numericOperators } from "../constants";
13 |
14 |
15 | type FilterConditionProps = {
16 | clauseId: string,
17 | path: string[],
18 | }
19 |
20 | const FilterCondition = ({ clauseId, path }: FilterConditionProps) => {
21 | const [clauses, setClauses] = useRecoilState(clauseState);
22 | const setTree = useSetRecoilState(treeState);
23 |
24 | const schema = useRecoilValue(schemaState);
25 |
26 | const condition = useMemo(() => clauses.get(clauseId) as ConditionClause, [clauses, clauseId]);
27 |
28 | const changeField = useCallback((oldField: string, currentOp: Operation, newField: string) => {
29 | const oldFieldDef = schema.find(c => c.field === oldField);
30 | const newFieldDef = schema.find(c => c.field === newField);
31 |
32 | setClauses(old => old.update(clauseId, c => {
33 | const condition = { ...c as ConditionClause };
34 | condition.field = newField;
35 | condition.default = false;
36 |
37 | if (oldFieldDef && newFieldDef) {
38 | // reset value if fields have different types
39 | if (oldFieldDef.type !== newFieldDef.type) {
40 | condition.value = "";
41 | }
42 |
43 | // reset operator if new field doesn't support current operator
44 | if (newFieldDef.filterOperators && !newFieldDef.filterOperators.includes(currentOp)) {
45 | condition.op = newFieldDef.filterOperators[0] ?? "eq";
46 | }
47 |
48 | // set collection field if new field is a collection
49 | if (newFieldDef.collection === true && newFieldDef.collectionFields) {
50 | condition.collectionField = newFieldDef.collectionFields[0].field;
51 | condition.collectionOp = "any";
52 |
53 | if (newFieldDef.collectionFields[0].filterOperators) {
54 | condition.op = newFieldDef.collectionFields[0].filterOperators[0];
55 | }
56 | else {
57 | condition.op = "eq";
58 | }
59 | } else { // clear collection fields if new field is not a collection
60 | condition.collectionField = undefined;
61 | condition.collectionOp = undefined;
62 | }
63 | }
64 |
65 | return condition;
66 | }));
67 | }, [schema, setClauses, clauseId]);
68 |
69 | const changeOp = useCallback((o: Operation) => {
70 | setClauses(old => old.update(clauseId, c => ({ ...c as ConditionClause, op: o, default: false })));
71 | }, [setClauses, clauseId]);
72 |
73 | const changeValue = useCallback((v: any) => {
74 | setClauses(old => old.update(clauseId, c => ({ ...c as ConditionClause, value: v, default: false })));
75 | }, [setClauses, clauseId]);
76 |
77 | const changeCollectionOp = useCallback((o: CollectionOperation) => {
78 | setClauses(old => old.update(clauseId, c => {
79 | const condition = { ...c as ConditionClause, collectionOp: o, default: false };
80 |
81 | // reset field operator if switching to count operator and current op is not valid
82 | if (o === "count" && !numericOperators.includes(condition.op)) {
83 | condition.op = "eq";
84 | }
85 |
86 | return condition;
87 | }));
88 | }, [setClauses, clauseId]);
89 |
90 | const changeCollectionField = useCallback((field: string, oldColField: string | undefined, currentOp: Operation, newColField: string | undefined) => {
91 | const fieldDef = schema.find(c => c.field === field);
92 |
93 | setClauses(old => old.update(clauseId, c => {
94 | const condition = { ...c as ConditionClause };
95 | condition.collectionField = newColField;
96 | condition.default = false;
97 |
98 | if (fieldDef && fieldDef.collectionFields && oldColField && newColField) {
99 | const oldColFieldDef = fieldDef.collectionFields.find(c => c.field === oldColField);
100 | const newColFieldDef = fieldDef.collectionFields.find(c => c.field === newColField);
101 |
102 | // reset value if fields have different types
103 | if (oldColFieldDef!.type !== newColFieldDef!.type) {
104 | condition.value = "";
105 | }
106 |
107 | // reset operator if new field doesn't support current operator
108 | if (newColFieldDef!.filterOperators && !newColFieldDef!.filterOperators.includes(currentOp)) {
109 | condition.op = newColFieldDef!.filterOperators[0] ?? "eq";
110 | }
111 | }
112 |
113 | return condition;
114 | }));
115 | }, [schema, setClauses, clauseId]);
116 |
117 | const remove = useCallback(() => {
118 | // if not root group
119 | if (path.length > 2) {
120 | setTree(oldTree => oldTree.withMutations((old) => {
121 | // delete self
122 | old.deleteIn([...path, clauseId]);
123 |
124 | // get path to parent node (i.e. remove "children" from end of path)
125 | const parentPath = [...path];
126 | parentPath.splice(-1, 1);
127 |
128 | do {
129 | const node = old.getIn(parentPath) as TreeGroup;
130 | // delete parent if now empty
131 | if (node && node.children.count() < 1) {
132 | old.deleteIn(parentPath);
133 | } else { // not the only child, so only remove self and stop
134 | old.deleteIn([...path, clauseId]);
135 | break;
136 | }
137 |
138 | parentPath.splice(-2, 2); // move up in path to next parent
139 | } while (parentPath.length > 2) // keep removing empty groups until root is reached
140 | }))
141 | } else {
142 | setTree(old => old.deleteIn([...path, clauseId]));
143 | }
144 |
145 | setClauses(old => old.remove(clauseId));
146 | }, [setClauses, setTree, clauseId, path])
147 |
148 | if (!condition) {
149 | return null;
150 | }
151 |
152 | return (
153 |
154 |
167 |
168 |
169 |
170 |
171 |
172 |
173 | );
174 | }
175 |
176 | export default FilterCondition;
--------------------------------------------------------------------------------
/packages/o-data-grid-pro/dev/App.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { CssBaseline, Typography, Grid, TextField, Slider, Chip } from "@mui/material";
3 | import { createTheme, ThemeProvider } from "@mui/material/styles";
4 | import { GridSortModel } from "@mui/x-data-grid"
5 | import { ODataGridColDef, ODataColumnVisibilityModel, escapeODataString } from "../src/index";
6 | import ODataGridPro from "../src/ODataGridPro";
7 | import { CacheProvider } from "@emotion/react";
8 | import createCache from "@emotion/cache";
9 | import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
10 | import { Dayjs } from "dayjs";
11 | import { ExternalBuilderProps } from "../../base/FilterBuilder/types";
12 |
13 | const theme = createTheme({
14 | palette: {
15 | mode: "dark"
16 | }
17 | })
18 |
19 | export const muiCache = createCache({
20 | key: "mui",
21 | prepend: true
22 | });
23 |
24 | const App = () => {
25 | return (
26 |
27 |
28 |
29 | row.Id}
34 | defaultSortModel={defaultSort}
35 | filterBuilderProps={filterBuilderProps}
36 | alwaysSelect={alwaysFetch}
37 | />
38 |
39 |
40 | );
41 | }
42 |
43 | type LocationFilter = {
44 | location?: string,
45 | distance?: number
46 | }
47 |
48 | const filterBuilderProps: ExternalBuilderProps = { autocompleteGroups: ["Job", "Company"], localizationProviderProps: { dateAdapter: AdapterDayjs } };
49 |
50 | const alwaysFetch = ["Id", "Archived"];
51 | const columns: ODataGridColDef[] = [
52 | {
53 | field: "Title",
54 | headerName: "Job Title",
55 | flex: 2,
56 | autocompleteGroup: "Job"
57 | },
58 | {
59 | field: "Location",
60 | headerName: "Location",
61 | flex: 1,
62 | renderCustomFilter: (value, setValue) => (
63 |
64 |
65 | setValue({ ...value, location: e.target.value })}
68 | size="small"
69 | fullWidth
70 | label="Search Location"
71 | required
72 | />
73 |
74 |
75 | Distance
76 | setValue({ ...value, distance: val as number })}
79 | step={5}
80 | min={0}
81 | max={50}
82 | valueLabelFormat={(val) => `${val}mi`}
83 | valueLabelDisplay="auto"
84 | size="small"
85 | sx={{padding: 0}}
86 | />
87 |
88 |
89 | ),
90 | getCustomFilterString: (_, v) => {
91 | const filter = v as LocationFilter;
92 | return {
93 | filter: `Latitude ne null and Longitude ne null and Distance le ${filter.distance ?? 15}`,
94 | compute: {
95 | compute: `geocode('${escapeODataString(filter.location ?? "")}', Latitude, Longitude) as Distance`,
96 | select: ["Distance"]
97 | }
98 | };
99 | },
100 | valueGetter: (params) => `${params.row.Location}${params.row.Distance ? ` (${params.row.Distance.toFixed(1)} mi away)` : ""}`,
101 | autocompleteGroup: "Job"
102 | },
103 | {
104 | field: "Company/Name",
105 | headerName: "Company",
106 | flex: 2,
107 | renderCell: (params) => (
108 |
109 |
110 | {params.value}
111 |
112 | {params.row["Company/Recruiter"] && }
113 | {params.row["Company/Blacklisted"] && }
114 |
115 | ),
116 | expand: { navigationField: "Company", select: "Id,Name,Recruiter,Blacklisted,Watched" },
117 | autocompleteGroup: "Company"
118 | },
119 | {
120 | field: "Salary",
121 | type: "number",
122 | filterField: "AvgYearlySalary",
123 | sortField: "AvgYearlySalary",
124 | label: "Median Annual Salary",
125 | filterType: "number",
126 | filterOperators: ["eq", "ne", "gt", "lt", "ge", "le", "null", "notnull"],
127 | flex: 1,
128 | autocompleteGroup: "Job"
129 | },
130 | {
131 | field: "Status",
132 | type: "singleSelect",
133 | valueOptions: ["Not Applied", "Awaiting Response", "In Progress", "Rejected", "Dropped Out"],
134 | filterOperators: ["eq", "ne"],
135 | autocompleteGroup: "Job"
136 | },
137 | {
138 | field: "JobCategories",
139 | headerName: "Categories",
140 | label: "Category",
141 | expand: {
142 | navigationField: "JobCategories/Category",
143 | select: "Name"
144 | },
145 | sortable: false,
146 | filterable: false,
147 | flex: 1,
148 | renderCell: (params) => params.row.JobCategories.map((c: any) => c["Category/Name"]).join(", "),
149 | autocompleteGroup: "Job"
150 | },
151 | {
152 | field: "Source/DisplayName",
153 | expand: { navigationField: "Source", select: "DisplayName" },
154 | headerName: "Source",
155 | filterable: false,
156 | sortable: false,
157 | flex: 1,
158 | valueGetter: (params) => params.row[params.field] ? params.row[params.field] : "Added Manually",
159 | autocompleteGroup: "Job"
160 | },
161 | {
162 | field: "Posted",
163 | select: "Posted,Seen,Archived",
164 | headerName: "Posted",
165 | type: "date",
166 | flex: .9,
167 | autocompleteGroup: "Job"
168 | },
169 |
170 | // filter only
171 | {
172 | field: "Company/Recruiter",
173 | label: "Company Type",
174 | filterOnly: true,
175 | filterOperators: ["eq", "ne"],
176 | type: "singleSelect",
177 | valueOptions: [
178 | { label: "Employer", value: false },
179 | { label: "Recruiter", value: true }
180 | ],
181 | autocompleteGroup: "Company"
182 | },
183 | {
184 | field: "Company/Watched",
185 | label: "Company Watched",
186 | filterOnly: true,
187 | filterOperators: ["eq", "ne"],
188 | type: "boolean",
189 | autocompleteGroup: "Company"
190 | },
191 | {
192 | field: "Company/Blacklisted",
193 | label: "Company Blacklisted",
194 | filterOnly: true,
195 | filterOperators: ["eq", "ne"],
196 | type: "boolean",
197 | autocompleteGroup: "Company"
198 | },
199 | {
200 | field: "Description",
201 | filterOnly: true,
202 | filterOperators: ["contains"],
203 | autocompleteGroup: "Job"
204 | },
205 | {
206 | field: "Notes",
207 | filterOnly: true,
208 | filterOperators: ["contains"],
209 | autocompleteGroup: "Job"
210 | }
211 | ];
212 |
213 | const columnVisibility: ODataColumnVisibilityModel = {
214 | "Company/Name": { xs: false, md: true },
215 | "Salary": { xs: false, lg: true },
216 | "Status": false,
217 | "JobCategories": { xs: false, xl: true },
218 | "Source/DisplayName": true,
219 | "Posted": { xs: false, sm: true },
220 | }
221 |
222 | const defaultSort: GridSortModel = [{ field: "Posted", sort: "desc" }];
223 |
224 | export default App;
--------------------------------------------------------------------------------
/packages/base/FilterBuilder/components/FilterRoot.tsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment, useCallback, useEffect, useState } from "react"
2 | import { useSetRecoilState } from "recoil";
3 | import { ArrowDropDown } from "@mui/icons-material";
4 | import { Button, ButtonGroup, Grid, MenuItem, MenuList, Paper, Popover } from "@mui/material";
5 |
6 | import FilterGroup from "./FilterGroup";
7 |
8 | import { clauseState, propsState, schemaState, treeState } from "../state"
9 |
10 | import { initialClauses, initialTree, rootConditionUuid, rootGroupUuid } from "../constants"
11 | import { FilterBuilderProps } from "./FilterBuilder";
12 | import { UseODataFilter, UseODataFilterWithState } from "../hooks";
13 | import { useMountEffect } from "../../hooks";
14 | import { ConditionClause, SerialisedGroup, QueryStringCollection } from "../types";
15 | import { deserialise } from "../utils";
16 |
17 | type FilterRootProps = {
18 | props: FilterBuilderProps
19 | }
20 |
21 | const FilterRoot = ({ props }: FilterRootProps) => {
22 | const setClauses = useSetRecoilState(clauseState);
23 | const setProps = useSetRecoilState(propsState);
24 | const setSchema = useSetRecoilState(schemaState);
25 | const setTree = useSetRecoilState(treeState);
26 |
27 | const odataFilter = UseODataFilter();
28 | const odataFilterWithState = UseODataFilterWithState();
29 |
30 | const [anchor, setAnchor] = useState(null);
31 |
32 | const { onSubmit, onRestoreState, disableHistory, filter: propsFilter } = props;
33 | const submit = useCallback((e: React.FormEvent) => {
34 | e.preventDefault();
35 | if (onSubmit) {
36 | const result = odataFilter();
37 |
38 | if (result.filter) {
39 | const returned = onSubmit({ ...result, filter: result.filter });
40 |
41 | if (disableHistory !== true) {
42 | window.history.pushState(
43 | {
44 | ...window.history.state,
45 | ...returned,
46 | filterBuilder: {
47 | filter: result.filter,
48 | compute: result.compute,
49 | select: result.select,
50 | serialised: result.serialised,
51 | queryString: result.queryString
52 | }
53 | },
54 | ""
55 | );
56 | }
57 | }
58 | }
59 | }, [onSubmit, odataFilter, disableHistory]);
60 |
61 | const reset = useCallback(() => {
62 | setClauses(initialClauses.update(rootConditionUuid, (c) => ({ ...c as ConditionClause, field: props.schema[0].field })));
63 | setTree(initialTree);
64 |
65 | if (onSubmit) {
66 | onSubmit({ filter: "" });
67 | }
68 |
69 | if (disableHistory !== true) {
70 | window.history.pushState({
71 | ...window.history.state,
72 | filterBuilder: {
73 | reset: true
74 | }
75 | }, "");
76 | }
77 | }, [setClauses, setTree, onSubmit, props.schema, disableHistory]);
78 |
79 | const handleReset = useCallback(() => reset(), [reset]);
80 |
81 | useEffect(() => {
82 | setSchema(props.schema);
83 | }, [props.schema, setSchema]);
84 |
85 | const restoreDefault = useCallback(() => {
86 | setClauses(initialClauses.update(rootConditionUuid, (c) => ({ ...c as ConditionClause, field: props.schema[0].field })));
87 | setTree(initialTree);
88 | }, [props.schema, setClauses, setTree]);
89 |
90 | const restoreState = useCallback((state: any, isPopstate: boolean) => {
91 | let filter = "", serialised, queryString, compute, select;
92 |
93 | if (state?.filterBuilder) {
94 | if (state.filterBuilder.reset === true && isPopstate === true) {
95 | restoreDefault();
96 | }
97 |
98 | compute = state.filterBuilder.compute as string;
99 | filter = state.filterBuilder.filter as string;
100 | select = state.filterBuilder.select as string[];
101 | serialised = state.filterBuilder.serialised as SerialisedGroup;
102 | queryString = state.filterBuilder.queryString as QueryStringCollection;
103 | } else {
104 | restoreDefault();
105 | }
106 |
107 | if (filter && serialised) {
108 | const [tree, clauses] = deserialise(serialised);
109 |
110 | setClauses(clauses);
111 | setTree(tree);
112 | }
113 |
114 | if (onRestoreState) {
115 | onRestoreState({ compute, filter, queryString, select, serialised}, state);
116 | }
117 | }, [onRestoreState, restoreDefault, setClauses, setTree]);
118 |
119 | const restoreFilter = useCallback((serialised: SerialisedGroup) => {
120 | const [tree, clauses] = deserialise(serialised);
121 |
122 | setClauses(clauses);
123 | setTree(tree);
124 |
125 | if (onRestoreState) {
126 | const result = odataFilterWithState(clauses, tree);
127 |
128 | onRestoreState({ ...result, filter: result.filter ?? "" });
129 | }
130 | }, [setClauses, setTree, onRestoreState, odataFilterWithState]);
131 |
132 | useEffect(() => {
133 | if (disableHistory !== true) {
134 | const handlePopState = (e: PopStateEvent) => { restoreState(e.state, true); };
135 |
136 | window.addEventListener("popstate", handlePopState);
137 | return () => window.removeEventListener("popstate", handlePopState);
138 | }
139 | }, [disableHistory, restoreState]);
140 |
141 | useEffect(() => {
142 | if (propsFilter) {
143 | restoreFilter(propsFilter);
144 | } else {
145 | restoreDefault();
146 | }
147 | }, [propsFilter, restoreFilter, restoreDefault]);
148 |
149 | useMountEffect(() => {
150 | setProps(props);
151 |
152 | // restore query from history state if enabled
153 | if (disableHistory !== true && window.history.state && window.history.state.filterBuilder) {
154 | restoreState(window.history.state, false);
155 | } else if (propsFilter) {
156 | restoreFilter(propsFilter);
157 | } else {
158 | restoreDefault();
159 | }
160 | });
161 |
162 | return (
163 |
164 |
216 |
217 |
218 | );
219 | }
220 |
221 | export default FilterRoot;
--------------------------------------------------------------------------------
/docs/api/Migrations/20230325165107_InitialCreate.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.EntityFrameworkCore.Migrations;
3 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
4 |
5 | #nullable disable
6 |
7 | namespace api.Migrations
8 | {
9 | public partial class InitialCreate : Migration
10 | {
11 | protected override void Up(MigrationBuilder migrationBuilder)
12 | {
13 | migrationBuilder.CreateTable(
14 | name: "Customers",
15 | columns: table => new
16 | {
17 | Id = table.Column(type: "integer", nullable: false)
18 | .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
19 | FirstName = table.Column(type: "text", nullable: false),
20 | MiddleNames = table.Column(type: "text", nullable: true),
21 | Surname = table.Column(type: "text", nullable: false),
22 | EmailAddress = table.Column(type: "text", nullable: false),
23 | CreatedDate = table.Column(type: "timestamp with time zone", nullable: false)
24 | },
25 | constraints: table =>
26 | {
27 | table.PrimaryKey("PK_Customers", x => x.Id);
28 | });
29 |
30 | migrationBuilder.CreateTable(
31 | name: "Product",
32 | columns: table => new
33 | {
34 | Id = table.Column(type: "integer", nullable: false)
35 | .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
36 | Name = table.Column(type: "text", nullable: false),
37 | Price = table.Column(type: "numeric", nullable: false)
38 | },
39 | constraints: table =>
40 | {
41 | table.PrimaryKey("PK_Product", x => x.Id);
42 | });
43 |
44 | migrationBuilder.CreateTable(
45 | name: "Addresses",
46 | columns: table => new
47 | {
48 | Id = table.Column(type: "integer", nullable: false)
49 | .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
50 | CustomerId = table.Column(type: "integer", nullable: false),
51 | Line1 = table.Column(type: "text", nullable: false),
52 | Line2 = table.Column(type: "text", nullable: true),
53 | Line3 = table.Column(type: "text", nullable: true),
54 | Town = table.Column(type: "text", nullable: false),
55 | County = table.Column(type: "text", nullable: true),
56 | Country = table.Column(type: "text", nullable: false),
57 | PostCode = table.Column(type: "text", nullable: false)
58 | },
59 | constraints: table =>
60 | {
61 | table.PrimaryKey("PK_Addresses", x => x.Id);
62 | table.ForeignKey(
63 | name: "FK_Addresses_Customers_CustomerId",
64 | column: x => x.CustomerId,
65 | principalTable: "Customers",
66 | principalColumn: "Id",
67 | onDelete: ReferentialAction.Cascade);
68 | });
69 |
70 | migrationBuilder.CreateTable(
71 | name: "Orders",
72 | columns: table => new
73 | {
74 | Id = table.Column(type: "integer", nullable: false)
75 | .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
76 | CustomerId = table.Column(type: "integer", nullable: false),
77 | DeliveryAddressId = table.Column(type: "integer", nullable: false),
78 | Total = table.Column(type: "numeric", nullable: false)
79 | },
80 | constraints: table =>
81 | {
82 | table.PrimaryKey("PK_Orders", x => x.Id);
83 | table.ForeignKey(
84 | name: "FK_Orders_Addresses_DeliveryAddressId",
85 | column: x => x.DeliveryAddressId,
86 | principalTable: "Addresses",
87 | principalColumn: "Id",
88 | onDelete: ReferentialAction.Cascade);
89 | table.ForeignKey(
90 | name: "FK_Orders_Customers_CustomerId",
91 | column: x => x.CustomerId,
92 | principalTable: "Customers",
93 | principalColumn: "Id",
94 | onDelete: ReferentialAction.Cascade);
95 | });
96 |
97 | migrationBuilder.CreateTable(
98 | name: "OrderProduct",
99 | columns: table => new
100 | {
101 | Id = table.Column(type: "integer", nullable: false)
102 | .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
103 | OrderId = table.Column(type: "integer", nullable: false),
104 | ProductId = table.Column(type: "integer", nullable: false),
105 | Quantity = table.Column(type: "integer", nullable: false)
106 | },
107 | constraints: table =>
108 | {
109 | table.PrimaryKey("PK_OrderProduct", x => x.Id);
110 | table.ForeignKey(
111 | name: "FK_OrderProduct_Orders_OrderId",
112 | column: x => x.OrderId,
113 | principalTable: "Orders",
114 | principalColumn: "Id",
115 | onDelete: ReferentialAction.Cascade);
116 | table.ForeignKey(
117 | name: "FK_OrderProduct_Product_ProductId",
118 | column: x => x.ProductId,
119 | principalTable: "Product",
120 | principalColumn: "Id",
121 | onDelete: ReferentialAction.Cascade);
122 | });
123 |
124 | migrationBuilder.CreateIndex(
125 | name: "IX_Addresses_CustomerId",
126 | table: "Addresses",
127 | column: "CustomerId");
128 |
129 | migrationBuilder.CreateIndex(
130 | name: "IX_OrderProduct_OrderId",
131 | table: "OrderProduct",
132 | column: "OrderId");
133 |
134 | migrationBuilder.CreateIndex(
135 | name: "IX_OrderProduct_ProductId",
136 | table: "OrderProduct",
137 | column: "ProductId");
138 |
139 | migrationBuilder.CreateIndex(
140 | name: "IX_Orders_CustomerId",
141 | table: "Orders",
142 | column: "CustomerId");
143 |
144 | migrationBuilder.CreateIndex(
145 | name: "IX_Orders_DeliveryAddressId",
146 | table: "Orders",
147 | column: "DeliveryAddressId");
148 | }
149 |
150 | protected override void Down(MigrationBuilder migrationBuilder)
151 | {
152 | migrationBuilder.DropTable(
153 | name: "OrderProduct");
154 |
155 | migrationBuilder.DropTable(
156 | name: "Orders");
157 |
158 | migrationBuilder.DropTable(
159 | name: "Product");
160 |
161 | migrationBuilder.DropTable(
162 | name: "Addresses");
163 |
164 | migrationBuilder.DropTable(
165 | name: "Customers");
166 | }
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/docs/api/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Ww][Ii][Nn]32/
27 | [Aa][Rr][Mm]/
28 | [Aa][Rr][Mm]64/
29 | bld/
30 | [Bb]in/
31 | [Oo]bj/
32 | [Ll]og/
33 | [Ll]ogs/
34 |
35 | # Visual Studio 2015/2017 cache/options directory
36 | .vs/
37 | # Uncomment if you have tasks that create the project's static files in wwwroot
38 | #wwwroot/
39 |
40 | # Visual Studio 2017 auto generated files
41 | Generated\ Files/
42 |
43 | # MSTest test Results
44 | [Tt]est[Rr]esult*/
45 | [Bb]uild[Ll]og.*
46 |
47 | # NUnit
48 | *.VisualState.xml
49 | TestResult.xml
50 | nunit-*.xml
51 |
52 | # Build Results of an ATL Project
53 | [Dd]ebugPS/
54 | [Rr]eleasePS/
55 | dlldata.c
56 |
57 | # Benchmark Results
58 | BenchmarkDotNet.Artifacts/
59 |
60 | # .NET Core
61 | project.lock.json
62 | project.fragment.lock.json
63 | artifacts/
64 |
65 | # ASP.NET Scaffolding
66 | ScaffoldingReadMe.txt
67 |
68 | # StyleCop
69 | StyleCopReport.xml
70 |
71 | # Files built by Visual Studio
72 | *_i.c
73 | *_p.c
74 | *_h.h
75 | *.ilk
76 | *.meta
77 | *.obj
78 | *.iobj
79 | *.pch
80 | *.pdb
81 | *.ipdb
82 | *.pgc
83 | *.pgd
84 | *.rsp
85 | *.sbr
86 | *.tlb
87 | *.tli
88 | *.tlh
89 | *.tmp
90 | *.tmp_proj
91 | *_wpftmp.csproj
92 | *.log
93 | *.tlog
94 | *.vspscc
95 | *.vssscc
96 | .builds
97 | *.pidb
98 | *.svclog
99 | *.scc
100 |
101 | # Chutzpah Test files
102 | _Chutzpah*
103 |
104 | # Visual C++ cache files
105 | ipch/
106 | *.aps
107 | *.ncb
108 | *.opendb
109 | *.opensdf
110 | *.sdf
111 | *.cachefile
112 | *.VC.db
113 | *.VC.VC.opendb
114 |
115 | # Visual Studio profiler
116 | *.psess
117 | *.vsp
118 | *.vspx
119 | *.sap
120 |
121 | # Visual Studio Trace Files
122 | *.e2e
123 |
124 | # TFS 2012 Local Workspace
125 | $tf/
126 |
127 | # Guidance Automation Toolkit
128 | *.gpState
129 |
130 | # ReSharper is a .NET coding add-in
131 | _ReSharper*/
132 | *.[Rr]e[Ss]harper
133 | *.DotSettings.user
134 |
135 | # TeamCity is a build add-in
136 | _TeamCity*
137 |
138 | # DotCover is a Code Coverage Tool
139 | *.dotCover
140 |
141 | # AxoCover is a Code Coverage Tool
142 | .axoCover/*
143 | !.axoCover/settings.json
144 |
145 | # Coverlet is a free, cross platform Code Coverage Tool
146 | coverage*.json
147 | coverage*.xml
148 | coverage*.info
149 |
150 | # Visual Studio code coverage results
151 | *.coverage
152 | *.coveragexml
153 |
154 | # NCrunch
155 | _NCrunch_*
156 | .*crunch*.local.xml
157 | nCrunchTemp_*
158 |
159 | # MightyMoose
160 | *.mm.*
161 | AutoTest.Net/
162 |
163 | # Web workbench (sass)
164 | .sass-cache/
165 |
166 | # Installshield output folder
167 | [Ee]xpress/
168 |
169 | # DocProject is a documentation generator add-in
170 | DocProject/buildhelp/
171 | DocProject/Help/*.HxT
172 | DocProject/Help/*.HxC
173 | DocProject/Help/*.hhc
174 | DocProject/Help/*.hhk
175 | DocProject/Help/*.hhp
176 | DocProject/Help/Html2
177 | DocProject/Help/html
178 |
179 | # Click-Once directory
180 | publish/
181 |
182 | # Publish Web Output
183 | *.[Pp]ublish.xml
184 | *.azurePubxml
185 | # Note: Comment the next line if you want to checkin your web deploy settings,
186 | # but database connection strings (with potential passwords) will be unencrypted
187 | *.pubxml
188 | *.publishproj
189 |
190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
191 | # checkin your Azure Web App publish settings, but sensitive information contained
192 | # in these scripts will be unencrypted
193 | PublishScripts/
194 |
195 | # NuGet Packages
196 | *.nupkg
197 | # NuGet Symbol Packages
198 | *.snupkg
199 | # The packages folder can be ignored because of Package Restore
200 | **/[Pp]ackages/*
201 | # except build/, which is used as an MSBuild target.
202 | !**/[Pp]ackages/build/
203 | # Uncomment if necessary however generally it will be regenerated when needed
204 | #!**/[Pp]ackages/repositories.config
205 | # NuGet v3's project.json files produces more ignorable files
206 | *.nuget.props
207 | *.nuget.targets
208 |
209 | # Microsoft Azure Build Output
210 | csx/
211 | *.build.csdef
212 |
213 | # Microsoft Azure Emulator
214 | ecf/
215 | rcf/
216 |
217 | # Windows Store app package directories and files
218 | AppPackages/
219 | BundleArtifacts/
220 | Package.StoreAssociation.xml
221 | _pkginfo.txt
222 | *.appx
223 | *.appxbundle
224 | *.appxupload
225 |
226 | # Visual Studio cache files
227 | # files ending in .cache can be ignored
228 | *.[Cc]ache
229 | # but keep track of directories ending in .cache
230 | !?*.[Cc]ache/
231 |
232 | # Others
233 | ClientBin/
234 | ~$*
235 | *~
236 | *.dbmdl
237 | *.dbproj.schemaview
238 | *.jfm
239 | *.pfx
240 | *.publishsettings
241 | orleans.codegen.cs
242 |
243 | # Including strong name files can present a security risk
244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
245 | #*.snk
246 |
247 | # Since there are multiple workflows, uncomment next line to ignore bower_components
248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
249 | #bower_components/
250 |
251 | # RIA/Silverlight projects
252 | Generated_Code/
253 |
254 | # Backup & report files from converting an old project file
255 | # to a newer Visual Studio version. Backup files are not needed,
256 | # because we have git ;-)
257 | _UpgradeReport_Files/
258 | Backup*/
259 | UpgradeLog*.XML
260 | UpgradeLog*.htm
261 | ServiceFabricBackup/
262 | *.rptproj.bak
263 |
264 | # SQL Server files
265 | *.mdf
266 | *.ldf
267 | *.ndf
268 |
269 | # Business Intelligence projects
270 | *.rdl.data
271 | *.bim.layout
272 | *.bim_*.settings
273 | *.rptproj.rsuser
274 | *- [Bb]ackup.rdl
275 | *- [Bb]ackup ([0-9]).rdl
276 | *- [Bb]ackup ([0-9][0-9]).rdl
277 |
278 | # Microsoft Fakes
279 | FakesAssemblies/
280 |
281 | # GhostDoc plugin setting file
282 | *.GhostDoc.xml
283 |
284 | # Node.js Tools for Visual Studio
285 | .ntvs_analysis.dat
286 | node_modules/
287 |
288 | # Visual Studio 6 build log
289 | *.plg
290 |
291 | # Visual Studio 6 workspace options file
292 | *.opt
293 |
294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
295 | *.vbw
296 |
297 | # Visual Studio 6 auto-generated project file (contains which files were open etc.)
298 | *.vbp
299 |
300 | # Visual Studio 6 workspace and project file (working project files containing files to include in project)
301 | *.dsw
302 | *.dsp
303 |
304 | # Visual Studio 6 technical files
305 | *.ncb
306 | *.aps
307 |
308 | # Visual Studio LightSwitch build output
309 | **/*.HTMLClient/GeneratedArtifacts
310 | **/*.DesktopClient/GeneratedArtifacts
311 | **/*.DesktopClient/ModelManifest.xml
312 | **/*.Server/GeneratedArtifacts
313 | **/*.Server/ModelManifest.xml
314 | _Pvt_Extensions
315 |
316 | # Paket dependency manager
317 | .paket/paket.exe
318 | paket-files/
319 |
320 | # FAKE - F# Make
321 | .fake/
322 |
323 | # CodeRush personal settings
324 | .cr/personal
325 |
326 | # Python Tools for Visual Studio (PTVS)
327 | __pycache__/
328 | *.pyc
329 |
330 | # Cake - Uncomment if you are using it
331 | # tools/**
332 | # !tools/packages.config
333 |
334 | # Tabs Studio
335 | *.tss
336 |
337 | # Telerik's JustMock configuration file
338 | *.jmconfig
339 |
340 | # BizTalk build output
341 | *.btp.cs
342 | *.btm.cs
343 | *.odx.cs
344 | *.xsd.cs
345 |
346 | # OpenCover UI analysis results
347 | OpenCover/
348 |
349 | # Azure Stream Analytics local run output
350 | ASALocalRun/
351 |
352 | # MSBuild Binary and Structured Log
353 | *.binlog
354 |
355 | # NVidia Nsight GPU debugger configuration file
356 | *.nvuser
357 |
358 | # MFractors (Xamarin productivity tool) working folder
359 | .mfractor/
360 |
361 | # Local History for Visual Studio
362 | .localhistory/
363 |
364 | # Visual Studio History (VSHistory) files
365 | .vshistory/
366 |
367 | # BeatPulse healthcheck temp database
368 | healthchecksdb
369 |
370 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
371 | MigrationBackup/
372 |
373 | # Ionide (cross platform F# VS Code tools) working folder
374 | .ionide/
375 |
376 | # Fody - auto-generated XML schema
377 | FodyWeavers.xsd
378 |
379 | # VS Code files for those working on multiple tools
380 | .vscode/*
381 | !.vscode/settings.json
382 | !.vscode/tasks.json
383 | !.vscode/launch.json
384 | !.vscode/extensions.json
385 | *.code-workspace
386 |
387 | # Local History for Visual Studio Code
388 | .history/
389 |
390 | # Windows Installer files from build outputs
391 | *.cab
392 | *.msi
393 | *.msix
394 | *.msm
395 | *.msp
396 |
397 | # JetBrains Rider
398 | *.sln.iml
399 |
--------------------------------------------------------------------------------
/docs/api/Migrations/20230325165107_InitialCreate.Designer.cs:
--------------------------------------------------------------------------------
1 | //
2 | using System;
3 | using Api.Data;
4 | using Microsoft.EntityFrameworkCore;
5 | using Microsoft.EntityFrameworkCore.Infrastructure;
6 | using Microsoft.EntityFrameworkCore.Migrations;
7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
8 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
9 |
10 | #nullable disable
11 |
12 | namespace api.Migrations
13 | {
14 | [DbContext(typeof(ApiContext))]
15 | [Migration("20230325165107_InitialCreate")]
16 | partial class InitialCreate
17 | {
18 | protected override void BuildTargetModel(ModelBuilder modelBuilder)
19 | {
20 | #pragma warning disable 612, 618
21 | modelBuilder
22 | .HasAnnotation("ProductVersion", "6.0.4")
23 | .HasAnnotation("Relational:MaxIdentifierLength", 63);
24 |
25 | NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
26 |
27 | modelBuilder.Entity("Api.Models.Address", b =>
28 | {
29 | b.Property("Id")
30 | .ValueGeneratedOnAdd()
31 | .HasColumnType("integer");
32 |
33 | NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
34 |
35 | b.Property("Country")
36 | .IsRequired()
37 | .HasColumnType("text");
38 |
39 | b.Property("County")
40 | .HasColumnType("text");
41 |
42 | b.Property("CustomerId")
43 | .HasColumnType("integer");
44 |
45 | b.Property("Line1")
46 | .IsRequired()
47 | .HasColumnType("text");
48 |
49 | b.Property("Line2")
50 | .HasColumnType("text");
51 |
52 | b.Property("Line3")
53 | .HasColumnType("text");
54 |
55 | b.Property("PostCode")
56 | .IsRequired()
57 | .HasColumnType("text");
58 |
59 | b.Property("Town")
60 | .IsRequired()
61 | .HasColumnType("text");
62 |
63 | b.HasKey("Id");
64 |
65 | b.HasIndex("CustomerId");
66 |
67 | b.ToTable("Addresses");
68 | });
69 |
70 | modelBuilder.Entity("Api.Models.Customer", b =>
71 | {
72 | b.Property("Id")
73 | .ValueGeneratedOnAdd()
74 | .HasColumnType("integer");
75 |
76 | NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
77 |
78 | b.Property("CreatedDate")
79 | .HasColumnType("timestamp with time zone");
80 |
81 | b.Property("EmailAddress")
82 | .IsRequired()
83 | .HasColumnType("text");
84 |
85 | b.Property("FirstName")
86 | .IsRequired()
87 | .HasColumnType("text");
88 |
89 | b.Property("MiddleNames")
90 | .HasColumnType("text");
91 |
92 | b.Property("Surname")
93 | .IsRequired()
94 | .HasColumnType("text");
95 |
96 | b.HasKey("Id");
97 |
98 | b.ToTable("Customers");
99 | });
100 |
101 | modelBuilder.Entity("Api.Models.Order", b =>
102 | {
103 | b.Property("Id")
104 | .ValueGeneratedOnAdd()
105 | .HasColumnType("integer");
106 |
107 | NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
108 |
109 | b.Property("CustomerId")
110 | .HasColumnType("integer");
111 |
112 | b.Property("DeliveryAddressId")
113 | .HasColumnType("integer");
114 |
115 | b.Property("Total")
116 | .HasColumnType("numeric");
117 |
118 | b.HasKey("Id");
119 |
120 | b.HasIndex("CustomerId");
121 |
122 | b.HasIndex("DeliveryAddressId");
123 |
124 | b.ToTable("Orders");
125 | });
126 |
127 | modelBuilder.Entity("Api.Models.OrderProduct", b =>
128 | {
129 | b.Property("Id")
130 | .ValueGeneratedOnAdd()
131 | .HasColumnType("integer");
132 |
133 | NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
134 |
135 | b.Property("OrderId")
136 | .HasColumnType("integer");
137 |
138 | b.Property("ProductId")
139 | .HasColumnType("integer");
140 |
141 | b.Property("Quantity")
142 | .HasColumnType("integer");
143 |
144 | b.HasKey("Id");
145 |
146 | b.HasIndex("OrderId");
147 |
148 | b.HasIndex("ProductId");
149 |
150 | b.ToTable("OrderProduct");
151 | });
152 |
153 | modelBuilder.Entity("Api.Models.Product", b =>
154 | {
155 | b.Property("Id")
156 | .ValueGeneratedOnAdd()
157 | .HasColumnType("integer");
158 |
159 | NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
160 |
161 | b.Property("Name")
162 | .IsRequired()
163 | .HasColumnType("text");
164 |
165 | b.Property("Price")
166 | .HasColumnType("numeric");
167 |
168 | b.HasKey("Id");
169 |
170 | b.ToTable("Product");
171 | });
172 |
173 | modelBuilder.Entity("Api.Models.Address", b =>
174 | {
175 | b.HasOne("Api.Models.Customer", "Customer")
176 | .WithMany("Addresses")
177 | .HasForeignKey("CustomerId")
178 | .OnDelete(DeleteBehavior.Cascade)
179 | .IsRequired();
180 |
181 | b.Navigation("Customer");
182 | });
183 |
184 | modelBuilder.Entity("Api.Models.Order", b =>
185 | {
186 | b.HasOne("Api.Models.Customer", "Customer")
187 | .WithMany("Orders")
188 | .HasForeignKey("CustomerId")
189 | .OnDelete(DeleteBehavior.Cascade)
190 | .IsRequired();
191 |
192 | b.HasOne("Api.Models.Address", "DeliveryAddress")
193 | .WithMany()
194 | .HasForeignKey("DeliveryAddressId")
195 | .OnDelete(DeleteBehavior.Cascade)
196 | .IsRequired();
197 |
198 | b.Navigation("Customer");
199 |
200 | b.Navigation("DeliveryAddress");
201 | });
202 |
203 | modelBuilder.Entity("Api.Models.OrderProduct", b =>
204 | {
205 | b.HasOne("Api.Models.Order", "Order")
206 | .WithMany("OrderProducts")
207 | .HasForeignKey("OrderId")
208 | .OnDelete(DeleteBehavior.Cascade)
209 | .IsRequired();
210 |
211 | b.HasOne("Api.Models.Product", "Product")
212 | .WithMany()
213 | .HasForeignKey("ProductId")
214 | .OnDelete(DeleteBehavior.Cascade)
215 | .IsRequired();
216 |
217 | b.Navigation("Order");
218 |
219 | b.Navigation("Product");
220 | });
221 |
222 | modelBuilder.Entity("Api.Models.Customer", b =>
223 | {
224 | b.Navigation("Addresses");
225 |
226 | b.Navigation("Orders");
227 | });
228 |
229 | modelBuilder.Entity("Api.Models.Order", b =>
230 | {
231 | b.Navigation("OrderProducts");
232 | });
233 | #pragma warning restore 612, 618
234 | }
235 | }
236 | }
237 |
--------------------------------------------------------------------------------
/docs/api/Migrations/ApiContextModelSnapshot.cs:
--------------------------------------------------------------------------------
1 | //
2 | using System;
3 | using Api.Data;
4 | using Microsoft.EntityFrameworkCore;
5 | using Microsoft.EntityFrameworkCore.Infrastructure;
6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
7 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
8 |
9 | #nullable disable
10 |
11 | namespace api.Migrations
12 | {
13 | [DbContext(typeof(ApiContext))]
14 | partial class ApiContextModelSnapshot : ModelSnapshot
15 | {
16 | protected override void BuildModel(ModelBuilder modelBuilder)
17 | {
18 | #pragma warning disable 612, 618
19 | modelBuilder
20 | .HasAnnotation("ProductVersion", "6.0.4")
21 | .HasAnnotation("Relational:MaxIdentifierLength", 63);
22 |
23 | NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
24 |
25 | modelBuilder.Entity("Api.Models.Address", b =>
26 | {
27 | b.Property("Id")
28 | .ValueGeneratedOnAdd()
29 | .HasColumnType("integer");
30 |
31 | NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
32 |
33 | b.Property("Country")
34 | .IsRequired()
35 | .HasColumnType("text");
36 |
37 | b.Property("County")
38 | .HasColumnType("text");
39 |
40 | b.Property("CustomerId")
41 | .HasColumnType("integer");
42 |
43 | b.Property("Line1")
44 | .IsRequired()
45 | .HasColumnType("text");
46 |
47 | b.Property("Line2")
48 | .HasColumnType("text");
49 |
50 | b.Property("Line3")
51 | .HasColumnType("text");
52 |
53 | b.Property("PostCode")
54 | .IsRequired()
55 | .HasColumnType("text");
56 |
57 | b.Property("Town")
58 | .IsRequired()
59 | .HasColumnType("text");
60 |
61 | b.HasKey("Id");
62 |
63 | b.HasIndex("CustomerId");
64 |
65 | b.ToTable("Addresses");
66 | });
67 |
68 | modelBuilder.Entity("Api.Models.Customer", b =>
69 | {
70 | b.Property("Id")
71 | .ValueGeneratedOnAdd()
72 | .HasColumnType("integer");
73 |
74 | NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
75 |
76 | b.Property("CreatedDate")
77 | .HasColumnType("timestamp with time zone");
78 |
79 | b.Property("EmailAddress")
80 | .IsRequired()
81 | .HasColumnType("text");
82 |
83 | b.Property("FirstName")
84 | .IsRequired()
85 | .HasColumnType("text");
86 |
87 | b.Property("MiddleNames")
88 | .HasColumnType("text");
89 |
90 | b.Property("Surname")
91 | .IsRequired()
92 | .HasColumnType("text");
93 |
94 | b.HasKey("Id");
95 |
96 | b.ToTable("Customers");
97 | });
98 |
99 | modelBuilder.Entity("Api.Models.Order", b =>
100 | {
101 | b.Property("Id")
102 | .ValueGeneratedOnAdd()
103 | .HasColumnType("integer");
104 |
105 | NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
106 |
107 | b.Property("CustomerId")
108 | .HasColumnType("integer");
109 |
110 | b.Property("Date")
111 | .HasColumnType("timestamp with time zone");
112 |
113 | b.Property("DeliveryAddressId")
114 | .HasColumnType("integer");
115 |
116 | b.Property("Total")
117 | .HasColumnType("numeric");
118 |
119 | b.HasKey("Id");
120 |
121 | b.HasIndex("CustomerId");
122 |
123 | b.HasIndex("DeliveryAddressId");
124 |
125 | b.ToTable("Orders");
126 | });
127 |
128 | modelBuilder.Entity("Api.Models.OrderProduct", b =>
129 | {
130 | b.Property("Id")
131 | .ValueGeneratedOnAdd()
132 | .HasColumnType("integer");
133 |
134 | NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
135 |
136 | b.Property("OrderId")
137 | .HasColumnType("integer");
138 |
139 | b.Property("ProductId")
140 | .HasColumnType("integer");
141 |
142 | b.Property("Quantity")
143 | .HasColumnType("integer");
144 |
145 | b.HasKey("Id");
146 |
147 | b.HasIndex("OrderId");
148 |
149 | b.HasIndex("ProductId");
150 |
151 | b.ToTable("OrderProduct");
152 | });
153 |
154 | modelBuilder.Entity("Api.Models.Product", b =>
155 | {
156 | b.Property("Id")
157 | .ValueGeneratedOnAdd()
158 | .HasColumnType("integer");
159 |
160 | NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
161 |
162 | b.Property("Name")
163 | .IsRequired()
164 | .HasColumnType("text");
165 |
166 | b.Property("Price")
167 | .HasColumnType("numeric");
168 |
169 | b.HasKey("Id");
170 |
171 | b.ToTable("Product");
172 | });
173 |
174 | modelBuilder.Entity("Api.Models.Address", b =>
175 | {
176 | b.HasOne("Api.Models.Customer", "Customer")
177 | .WithMany("Addresses")
178 | .HasForeignKey("CustomerId")
179 | .OnDelete(DeleteBehavior.Cascade)
180 | .IsRequired();
181 |
182 | b.Navigation("Customer");
183 | });
184 |
185 | modelBuilder.Entity("Api.Models.Order", b =>
186 | {
187 | b.HasOne("Api.Models.Customer", "Customer")
188 | .WithMany("Orders")
189 | .HasForeignKey("CustomerId")
190 | .OnDelete(DeleteBehavior.Cascade)
191 | .IsRequired();
192 |
193 | b.HasOne("Api.Models.Address", "DeliveryAddress")
194 | .WithMany()
195 | .HasForeignKey("DeliveryAddressId")
196 | .OnDelete(DeleteBehavior.Cascade)
197 | .IsRequired();
198 |
199 | b.Navigation("Customer");
200 |
201 | b.Navigation("DeliveryAddress");
202 | });
203 |
204 | modelBuilder.Entity("Api.Models.OrderProduct", b =>
205 | {
206 | b.HasOne("Api.Models.Order", "Order")
207 | .WithMany("OrderProducts")
208 | .HasForeignKey("OrderId")
209 | .OnDelete(DeleteBehavior.Cascade)
210 | .IsRequired();
211 |
212 | b.HasOne("Api.Models.Product", "Product")
213 | .WithMany()
214 | .HasForeignKey("ProductId")
215 | .OnDelete(DeleteBehavior.Cascade)
216 | .IsRequired();
217 |
218 | b.Navigation("Order");
219 |
220 | b.Navigation("Product");
221 | });
222 |
223 | modelBuilder.Entity("Api.Models.Customer", b =>
224 | {
225 | b.Navigation("Addresses");
226 |
227 | b.Navigation("Orders");
228 | });
229 |
230 | modelBuilder.Entity("Api.Models.Order", b =>
231 | {
232 | b.Navigation("OrderProducts");
233 | });
234 | #pragma warning restore 612, 618
235 | }
236 | }
237 | }
238 |
--------------------------------------------------------------------------------
/docs/api/Migrations/20230325170658_OrderDate.Designer.cs:
--------------------------------------------------------------------------------
1 | //
2 | using System;
3 | using Api.Data;
4 | using Microsoft.EntityFrameworkCore;
5 | using Microsoft.EntityFrameworkCore.Infrastructure;
6 | using Microsoft.EntityFrameworkCore.Migrations;
7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
8 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
9 |
10 | #nullable disable
11 |
12 | namespace api.Migrations
13 | {
14 | [DbContext(typeof(ApiContext))]
15 | [Migration("20230325170658_OrderDate")]
16 | partial class OrderDate
17 | {
18 | protected override void BuildTargetModel(ModelBuilder modelBuilder)
19 | {
20 | #pragma warning disable 612, 618
21 | modelBuilder
22 | .HasAnnotation("ProductVersion", "6.0.4")
23 | .HasAnnotation("Relational:MaxIdentifierLength", 63);
24 |
25 | NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
26 |
27 | modelBuilder.Entity("Api.Models.Address", b =>
28 | {
29 | b.Property("Id")
30 | .ValueGeneratedOnAdd()
31 | .HasColumnType("integer");
32 |
33 | NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
34 |
35 | b.Property("Country")
36 | .IsRequired()
37 | .HasColumnType("text");
38 |
39 | b.Property("County")
40 | .HasColumnType("text");
41 |
42 | b.Property("CustomerId")
43 | .HasColumnType("integer");
44 |
45 | b.Property("Line1")
46 | .IsRequired()
47 | .HasColumnType("text");
48 |
49 | b.Property("Line2")
50 | .HasColumnType("text");
51 |
52 | b.Property("Line3")
53 | .HasColumnType("text");
54 |
55 | b.Property("PostCode")
56 | .IsRequired()
57 | .HasColumnType("text");
58 |
59 | b.Property("Town")
60 | .IsRequired()
61 | .HasColumnType("text");
62 |
63 | b.HasKey("Id");
64 |
65 | b.HasIndex("CustomerId");
66 |
67 | b.ToTable("Addresses");
68 | });
69 |
70 | modelBuilder.Entity("Api.Models.Customer", b =>
71 | {
72 | b.Property("Id")
73 | .ValueGeneratedOnAdd()
74 | .HasColumnType("integer");
75 |
76 | NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
77 |
78 | b.Property