@sqrzro/addon-addresses 4.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +5 -0
- package/dist/forms/AddressForm/index.d.ts +7 -0
- package/dist/forms/AddressForm/index.js +14 -0
- package/dist/forms/AddressForm/interfaces.d.ts +10 -0
- package/dist/forms/AddressForm/interfaces.js +1 -0
- package/dist/forms/AddressForm/server.d.ts +5 -0
- package/dist/forms/AddressForm/server.js +29 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/interfaces.d.ts +2 -0
- package/dist/interfaces.js +2 -0
- package/dist/repositories/AddressRepository.d.ts +174 -0
- package/dist/repositories/AddressRepository.js +72 -0
- package/dist/repository/index.d.ts +17 -0
- package/dist/repository/index.js +42 -0
- package/dist/rules/postcode.d.ts +3 -0
- package/dist/rules/postcode.js +16 -0
- package/dist/schema.d.ts +163 -0
- package/dist/schema.js +15 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +2 -0
- package/dist/ui.d.ts +1 -0
- package/dist/ui.js +1 -0
- package/dist/utility/ids.d.ts +2 -0
- package/dist/utility/ids.js +1 -0
- package/dist/utility/interfaces.d.ts +13 -0
- package/dist/utility/interfaces.js +1 -0
- package/package.json +47 -0
- package/src/forms/AddressForm/index.tsx +31 -0
- package/src/forms/AddressForm/interfaces.ts +11 -0
- package/src/forms/AddressForm/server.ts +40 -0
- package/src/interfaces.ts +2 -0
- package/src/repositories/AddressRepository.ts +92 -0
- package/src/repository/index.ts +67 -0
- package/src/rules/postcode.ts +22 -0
- package/src/schema.ts +20 -0
- package/src/server.ts +2 -0
- package/src/ui.ts +1 -0
- package/src/utility/ids.ts +3 -0
- package/src/utility/interfaces.ts +16 -0
- package/tsconfig.json +20 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Copyright 2024 Richard Carter
|
|
2
|
+
|
|
3
|
+
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
|
4
|
+
|
|
5
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Default } from '@sqrzro/ui/forms';
|
|
2
|
+
import type { AddressFormFields } from './interfaces';
|
|
3
|
+
interface AddressFormProps {
|
|
4
|
+
defaults?: Default<AddressFormFields> | null;
|
|
5
|
+
}
|
|
6
|
+
declare function AddressForm({ defaults }: Readonly<AddressFormProps>): React.ReactElement;
|
|
7
|
+
export default AddressForm;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { EditableForm, EditableTextFormField, useEditableForm } from '@sqrzro/ui/forms';
|
|
4
|
+
import { formatPostcode } from '@sqrzro/utility';
|
|
5
|
+
import submit from './server';
|
|
6
|
+
function AddressForm({ defaults }) {
|
|
7
|
+
const { fieldProps, formProps } = useEditableForm({
|
|
8
|
+
defaults: defaults ?? {},
|
|
9
|
+
onSubmit: submit,
|
|
10
|
+
title: 'Address',
|
|
11
|
+
});
|
|
12
|
+
return (_jsxs(EditableForm, { ...formProps, children: [_jsx(EditableTextFormField, { ...fieldProps('line1', 'Line 1') }), _jsx(EditableTextFormField, { ...fieldProps('line2', 'Line 2') }), _jsx(EditableTextFormField, { ...fieldProps('city') }), _jsx(EditableTextFormField, { ...fieldProps('county') }), _jsx(EditableTextFormField, { ...fieldProps('postcode'), renderValue: formatPostcode })] }));
|
|
13
|
+
}
|
|
14
|
+
export default AddressForm;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { FormResponse } from '@sqrzro/server/forms';
|
|
2
|
+
import type { Address } from '../../utility/interfaces';
|
|
3
|
+
import type { AddressFormFields } from './interfaces';
|
|
4
|
+
declare function submit(formData: AddressFormFields): FormResponse<Address>;
|
|
5
|
+
export default submit;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use server';
|
|
2
|
+
import { submitForm } from '@sqrzro/server/forms';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import AddressRepository from '../../repositories/AddressRepository';
|
|
5
|
+
import getPostcodeRule from '../../rules/postcode';
|
|
6
|
+
const schema = z.object({
|
|
7
|
+
city: z.string().min(4).max(20).nonoptional(),
|
|
8
|
+
county: z.string().min(4).max(20).nonoptional(),
|
|
9
|
+
id: AddressRepository.getIDRule('id').validator.optional(),
|
|
10
|
+
line1: z.string().min(4).max(30).nonoptional(),
|
|
11
|
+
line2: z.string().max(30).nullable().optional(),
|
|
12
|
+
subjectID: z.string().nonoptional(),
|
|
13
|
+
postcode: getPostcodeRule().validator.nonoptional(),
|
|
14
|
+
});
|
|
15
|
+
async function fn(data) {
|
|
16
|
+
const { id, ...rest } = data;
|
|
17
|
+
if (typeof id !== 'undefined') {
|
|
18
|
+
return AddressRepository.update({ id, ...rest });
|
|
19
|
+
}
|
|
20
|
+
return AddressRepository.create(rest);
|
|
21
|
+
}
|
|
22
|
+
async function submit(formData) {
|
|
23
|
+
return submitForm({
|
|
24
|
+
fn,
|
|
25
|
+
formData,
|
|
26
|
+
schema,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export default submit;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * from './utility/ids';
|
|
2
|
+
export * from './utility/interfaces';
|
|
3
|
+
export { addressTable } from './schema';
|
|
4
|
+
export { default as AddressForm } from './forms/AddressForm';
|
|
5
|
+
export { default as AddressRepository } from './repositories/AddressRepository';
|
|
6
|
+
export { withAddresses, withAddressesConfig } from './repository';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * from './utility/ids';
|
|
2
|
+
export * from './utility/interfaces';
|
|
3
|
+
export { addressTable } from './schema';
|
|
4
|
+
export { default as AddressForm } from './forms/AddressForm';
|
|
5
|
+
export { default as AddressRepository } from './repositories/AddressRepository';
|
|
6
|
+
export { withAddresses, withAddressesConfig } from './repository';
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { AddressID } from '../utility/ids';
|
|
2
|
+
import type { Address, AddressData, AddressFilters } from '../utility/interfaces';
|
|
3
|
+
declare function ensureBySubjectID(subjectID: string): Promise<Address>;
|
|
4
|
+
declare function getBySubjectID(subjectID: string): Promise<Address | null>;
|
|
5
|
+
declare const _default: {
|
|
6
|
+
ensureBySubjectID: typeof ensureBySubjectID;
|
|
7
|
+
getBySubjectID: typeof getBySubjectID;
|
|
8
|
+
create: (data: AddressData) => Promise<{
|
|
9
|
+
id: `add_${string}`;
|
|
10
|
+
subjectID: string;
|
|
11
|
+
line1: string;
|
|
12
|
+
line2: string | null;
|
|
13
|
+
city: string;
|
|
14
|
+
county: string;
|
|
15
|
+
postcode: string;
|
|
16
|
+
createdAt: Date;
|
|
17
|
+
deletedAt: Date | null;
|
|
18
|
+
}>;
|
|
19
|
+
delete: (id: `add_${string}`) => Promise<void>;
|
|
20
|
+
ensureByID: (id: `add_${string}`, fnConfig?: import("@sqrzro/server/database").SelectFunctionConfig<Partial<AddressFilters> | null, never> | undefined) => Promise<{
|
|
21
|
+
id: `add_${string}`;
|
|
22
|
+
subjectID: string;
|
|
23
|
+
line1: string;
|
|
24
|
+
line2: string | null;
|
|
25
|
+
city: string;
|
|
26
|
+
county: string;
|
|
27
|
+
postcode: string;
|
|
28
|
+
createdAt: Date;
|
|
29
|
+
deletedAt: Date | null;
|
|
30
|
+
}>;
|
|
31
|
+
ensureByIndex: <K extends string>(key: K, value: any, fnConfig?: import("@sqrzro/server/database").SelectFunctionConfig<Partial<AddressFilters> | null, never> | undefined) => Promise<{
|
|
32
|
+
id: `add_${string}`;
|
|
33
|
+
subjectID: string;
|
|
34
|
+
line1: string;
|
|
35
|
+
line2: string | null;
|
|
36
|
+
city: string;
|
|
37
|
+
county: string;
|
|
38
|
+
postcode: string;
|
|
39
|
+
createdAt: Date;
|
|
40
|
+
deletedAt: Date | null;
|
|
41
|
+
}>;
|
|
42
|
+
ensureFromParams: (params?: Promise<Record<string, string>> | Record<string, string> | null, fnConfig?: import("@sqrzro/server/database").SelectFunctionConfig<Partial<AddressFilters> | null, never> | undefined) => Promise<{
|
|
43
|
+
id: `add_${string}`;
|
|
44
|
+
subjectID: string;
|
|
45
|
+
line1: string;
|
|
46
|
+
line2: string | null;
|
|
47
|
+
city: string;
|
|
48
|
+
county: string;
|
|
49
|
+
postcode: string;
|
|
50
|
+
createdAt: Date;
|
|
51
|
+
deletedAt: Date | null;
|
|
52
|
+
}>;
|
|
53
|
+
ensureFromSearchParams: (searchParams?: Promise<Record<string, string>> | Record<string, string> | null, fnConfig?: import("@sqrzro/server/database").SelectFunctionConfig<Partial<AddressFilters> | null, never> | undefined, key?: string) => Promise<{
|
|
54
|
+
id: `add_${string}`;
|
|
55
|
+
subjectID: string;
|
|
56
|
+
line1: string;
|
|
57
|
+
line2: string | null;
|
|
58
|
+
city: string;
|
|
59
|
+
county: string;
|
|
60
|
+
postcode: string;
|
|
61
|
+
createdAt: Date;
|
|
62
|
+
deletedAt: Date | null;
|
|
63
|
+
}>;
|
|
64
|
+
ensureID: (id: string | null | undefined) => `add_${string}`;
|
|
65
|
+
ensureIDFromParams: (params?: Promise<Record<string, string>> | Record<string, string> | null) => Promise<`add_${string}`>;
|
|
66
|
+
ensureIDFromSearchParams: (searchParams?: Promise<Record<string, string>> | Record<string, string> | null, key?: string) => Promise<`add_${string}`>;
|
|
67
|
+
getAll: (fnConfig?: import("@sqrzro/server/database").SelectFunctionConfig<Partial<AddressFilters> | null, never> | undefined) => Promise<{
|
|
68
|
+
id: `add_${string}`;
|
|
69
|
+
subjectID: string;
|
|
70
|
+
line1: string;
|
|
71
|
+
line2: string | null;
|
|
72
|
+
city: string;
|
|
73
|
+
county: string;
|
|
74
|
+
postcode: string;
|
|
75
|
+
createdAt: Date;
|
|
76
|
+
deletedAt: Date | null;
|
|
77
|
+
}[]>;
|
|
78
|
+
getAutocomplete: {
|
|
79
|
+
({ id, }: {
|
|
80
|
+
id?: `add_${string}` | undefined;
|
|
81
|
+
}, fnConfig?: import("@sqrzro/server/database").SelectFunctionConfig<Partial<AddressFilters> | null, never> | undefined): Promise<null>;
|
|
82
|
+
({ search, }: {
|
|
83
|
+
search?: string;
|
|
84
|
+
}, fnConfig?: import("@sqrzro/server/database").SelectFunctionConfig<Partial<AddressFilters> | null, never> | undefined): Promise<never[]>;
|
|
85
|
+
};
|
|
86
|
+
getByID: (id: `add_${string}`, fnConfig?: import("@sqrzro/server/database").SelectFunctionConfig<Partial<AddressFilters> | null, never> | undefined) => Promise<{
|
|
87
|
+
id: `add_${string}`;
|
|
88
|
+
subjectID: string;
|
|
89
|
+
line1: string;
|
|
90
|
+
line2: string | null;
|
|
91
|
+
city: string;
|
|
92
|
+
county: string;
|
|
93
|
+
postcode: string;
|
|
94
|
+
createdAt: Date;
|
|
95
|
+
deletedAt: Date | null;
|
|
96
|
+
} | null>;
|
|
97
|
+
getByIndex: <K extends string>(key: K, value: any, fnConfig?: import("@sqrzro/server/database").SelectFunctionConfig<Partial<AddressFilters> | null, never> | undefined) => Promise<{
|
|
98
|
+
id: `add_${string}`;
|
|
99
|
+
subjectID: string;
|
|
100
|
+
line1: string;
|
|
101
|
+
line2: string | null;
|
|
102
|
+
city: string;
|
|
103
|
+
county: string;
|
|
104
|
+
postcode: string;
|
|
105
|
+
createdAt: Date;
|
|
106
|
+
deletedAt: Date | null;
|
|
107
|
+
} | null>;
|
|
108
|
+
getDropdown: (fnConfig?: import("@sqrzro/server/database").SelectFunctionConfig<Partial<AddressFilters> | null, never> | undefined) => Promise<never[]>;
|
|
109
|
+
getFromParams: (params?: Promise<Record<string, string>> | Record<string, string> | null, fnConfig?: import("@sqrzro/server/database").SelectFunctionConfig<Partial<AddressFilters> | null, never> | undefined) => Promise<{
|
|
110
|
+
id: `add_${string}`;
|
|
111
|
+
subjectID: string;
|
|
112
|
+
line1: string;
|
|
113
|
+
line2: string | null;
|
|
114
|
+
city: string;
|
|
115
|
+
county: string;
|
|
116
|
+
postcode: string;
|
|
117
|
+
createdAt: Date;
|
|
118
|
+
deletedAt: Date | null;
|
|
119
|
+
} | null>;
|
|
120
|
+
getFromSearchParams: (searchParams?: Promise<Record<string, string>> | Record<string, string> | null, fnConfig?: import("@sqrzro/server/database").SelectFunctionConfig<Partial<AddressFilters> | null, never> | undefined, key?: string) => Promise<{
|
|
121
|
+
id: `add_${string}`;
|
|
122
|
+
subjectID: string;
|
|
123
|
+
line1: string;
|
|
124
|
+
line2: string | null;
|
|
125
|
+
city: string;
|
|
126
|
+
county: string;
|
|
127
|
+
postcode: string;
|
|
128
|
+
createdAt: Date;
|
|
129
|
+
deletedAt: Date | null;
|
|
130
|
+
} | null>;
|
|
131
|
+
getIDFromParams: (params?: Promise<Record<string, string>> | Record<string, string> | null) => Promise<`add_${string}` | null>;
|
|
132
|
+
getIDFromSearchParams: (searchParams?: Promise<Record<string, string>> | Record<string, string> | null, key?: string) => Promise<`add_${string}` | null>;
|
|
133
|
+
getIDRule: (idKey: string) => import("@sqrzro/server/database").ValidationRule<import("zod").ZodTemplateLiteral<`add_${string}`>, <T>(data: T) => Promise<T & Record<string, {
|
|
134
|
+
id: `add_${string}`;
|
|
135
|
+
subjectID: string;
|
|
136
|
+
line1: string;
|
|
137
|
+
line2: string | null;
|
|
138
|
+
city: string;
|
|
139
|
+
county: string;
|
|
140
|
+
postcode: string;
|
|
141
|
+
createdAt: Date;
|
|
142
|
+
deletedAt: Date | null;
|
|
143
|
+
}>>, undefined>;
|
|
144
|
+
getPaginated: (fnConfig?: import("@sqrzro/server/database").PaginatedFunctionConfig<Partial<AddressFilters> | null, never> | undefined) => Promise<import("@sqrzro/server/utility").Paginated<{
|
|
145
|
+
id: `add_${string}`;
|
|
146
|
+
subjectID: string;
|
|
147
|
+
line1: string;
|
|
148
|
+
line2: string | null;
|
|
149
|
+
city: string;
|
|
150
|
+
county: string;
|
|
151
|
+
postcode: string;
|
|
152
|
+
createdAt: Date;
|
|
153
|
+
deletedAt: Date | null;
|
|
154
|
+
}>>;
|
|
155
|
+
guardID: {
|
|
156
|
+
(id: (string | null | undefined)[]): `add_${string}`[];
|
|
157
|
+
(id: string | null | undefined): `add_${string}` | null;
|
|
158
|
+
};
|
|
159
|
+
undelete: (id: `add_${string}`) => Promise<void>;
|
|
160
|
+
update: (data: Partial<AddressData> & {
|
|
161
|
+
id: AddressID;
|
|
162
|
+
}) => Promise<{
|
|
163
|
+
id: `add_${string}`;
|
|
164
|
+
subjectID: string;
|
|
165
|
+
line1: string;
|
|
166
|
+
line2: string | null;
|
|
167
|
+
city: string;
|
|
168
|
+
county: string;
|
|
169
|
+
postcode: string;
|
|
170
|
+
createdAt: Date;
|
|
171
|
+
deletedAt: Date | null;
|
|
172
|
+
}>;
|
|
173
|
+
};
|
|
174
|
+
export default _default;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { db } from '@sqrzro/server/database';
|
|
2
|
+
import { createRepository } from '@sqrzro/server/database';
|
|
3
|
+
import { generateID } from '@sqrzro/server/utility';
|
|
4
|
+
import { pick } from '@sqrzro/utility';
|
|
5
|
+
import { and, eq, inArray } from 'drizzle-orm';
|
|
6
|
+
import { ADDRESS_PREFIX } from '../utility/ids';
|
|
7
|
+
import { addressTable } from '../schema';
|
|
8
|
+
const fillable = [
|
|
9
|
+
'city',
|
|
10
|
+
'county',
|
|
11
|
+
'line1',
|
|
12
|
+
'line2',
|
|
13
|
+
'postcode',
|
|
14
|
+
'subjectID',
|
|
15
|
+
];
|
|
16
|
+
const AddressRepository = createRepository({
|
|
17
|
+
async applyFilters(filters) {
|
|
18
|
+
const conditions = [];
|
|
19
|
+
if (filters?.id) {
|
|
20
|
+
conditions.push(inArray(addressTable.id, filters.id));
|
|
21
|
+
}
|
|
22
|
+
if (filters?.subjectID) {
|
|
23
|
+
if (Array.isArray(filters.subjectID)) {
|
|
24
|
+
conditions.push(inArray(addressTable.subjectID, filters.subjectID));
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
conditions.push(eq(addressTable.subjectID, filters.subjectID));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return conditions;
|
|
31
|
+
},
|
|
32
|
+
indexes: { id: addressTable.id, subjectID: addressTable.subjectID },
|
|
33
|
+
async makeQuery(conditions, config) {
|
|
34
|
+
return db
|
|
35
|
+
.select()
|
|
36
|
+
.from(addressTable)
|
|
37
|
+
.where(and(...conditions))
|
|
38
|
+
.limit(config.limit)
|
|
39
|
+
.offset(config.offset);
|
|
40
|
+
},
|
|
41
|
+
async makeDeleteMutation(id) {
|
|
42
|
+
await db.update(addressTable).set({ deletedAt: new Date() }).where(eq(addressTable.id, id));
|
|
43
|
+
},
|
|
44
|
+
async makeUndeleteMutation(id) {
|
|
45
|
+
await db.update(addressTable).set({ deletedAt: null }).where(eq(addressTable.id, id));
|
|
46
|
+
},
|
|
47
|
+
async makeInsertMutation(data) {
|
|
48
|
+
const values = {
|
|
49
|
+
id: generateID(ADDRESS_PREFIX),
|
|
50
|
+
...pick(data, fillable),
|
|
51
|
+
};
|
|
52
|
+
const [result] = await db.insert(addressTable).values(values).returning();
|
|
53
|
+
return result;
|
|
54
|
+
},
|
|
55
|
+
async makeUpdateMutation(data) {
|
|
56
|
+
const [result] = await db
|
|
57
|
+
.update(addressTable)
|
|
58
|
+
.set(data)
|
|
59
|
+
.where(eq(addressTable.id, data.id))
|
|
60
|
+
.returning();
|
|
61
|
+
return result;
|
|
62
|
+
},
|
|
63
|
+
paramKey: 'address',
|
|
64
|
+
prefix: ADDRESS_PREFIX,
|
|
65
|
+
});
|
|
66
|
+
function ensureBySubjectID(subjectID) {
|
|
67
|
+
return AddressRepository.ensureByIndex('subjectID', subjectID);
|
|
68
|
+
}
|
|
69
|
+
function getBySubjectID(subjectID) {
|
|
70
|
+
return AddressRepository.getByIndex('subjectID', subjectID);
|
|
71
|
+
}
|
|
72
|
+
export default { ...AddressRepository, ensureBySubjectID, getBySubjectID };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { RepositoryConfig, UnwrapModel } from '@sqrzro/server/database';
|
|
2
|
+
import { Address, AddressData, WithAddress } from '../utility/interfaces';
|
|
3
|
+
interface AddressesRepositoryConfig {
|
|
4
|
+
}
|
|
5
|
+
export declare function withAddressesConfig<T extends RepositoryConfig>(cfg: T): T & {
|
|
6
|
+
makeQuery: (conditions: import("@sqrzro/server/database").QueryConditions, config: import("@sqrzro/server/database").QueryConfig) => Promise<WithAddress<UnwrapModel<T>>[]>;
|
|
7
|
+
};
|
|
8
|
+
export declare function withAddresses(config: AddressesRepositoryConfig): <TRepo extends {
|
|
9
|
+
ensureByID: (...args: any[]) => any;
|
|
10
|
+
update: (...args: any[]) => any;
|
|
11
|
+
}>(repository: TRepo) => TRepo & {
|
|
12
|
+
attachAddress: (subjectID: string, data: AddressData) => Promise<Address>;
|
|
13
|
+
withAddress: <T extends {
|
|
14
|
+
id: string;
|
|
15
|
+
}>(subject: T) => Promise<WithAddress<T>>;
|
|
16
|
+
};
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import AddressRepository from '../repositories/AddressRepository';
|
|
2
|
+
export function withAddressesConfig(cfg) {
|
|
3
|
+
return {
|
|
4
|
+
...cfg,
|
|
5
|
+
makeQuery: async (...args) => {
|
|
6
|
+
const results = await cfg.makeQuery?.(...args);
|
|
7
|
+
const ids = results?.map((item) => item.id) ?? [];
|
|
8
|
+
const addresses = await AddressRepository.getAll({ filters: { subjectID: ids } });
|
|
9
|
+
return (results?.map((item) => ({
|
|
10
|
+
...item,
|
|
11
|
+
address: addresses.find((a) => a.subjectID === item.id) ?? null,
|
|
12
|
+
})) ?? []);
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function withAddresses(config) {
|
|
17
|
+
return function (repository) {
|
|
18
|
+
async function attachAddress(subjectID, data) {
|
|
19
|
+
const model = (await repository.ensureByID(subjectID));
|
|
20
|
+
if (model.addressID) {
|
|
21
|
+
return AddressRepository.update({ ...data, id: model.addressID });
|
|
22
|
+
}
|
|
23
|
+
const address = await AddressRepository.create(data);
|
|
24
|
+
await repository.update({ id: subjectID, addressID: address.id });
|
|
25
|
+
return address;
|
|
26
|
+
}
|
|
27
|
+
async function withAddress(subject) {
|
|
28
|
+
const [address] = await AddressRepository.getAll({
|
|
29
|
+
filters: { subjectID: subject.id },
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
...subject,
|
|
33
|
+
address: address ?? null,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
...repository,
|
|
38
|
+
attachAddress,
|
|
39
|
+
withAddress,
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createRule } from '@sqrzro/server/forms';
|
|
2
|
+
import z from 'zod';
|
|
3
|
+
function preprocess(value) {
|
|
4
|
+
if (typeof value === 'string') {
|
|
5
|
+
return value.replace(/[\s-]/g, '').toUpperCase();
|
|
6
|
+
}
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
function getPostcodeRule() {
|
|
10
|
+
const validator = z.preprocess(preprocess, z
|
|
11
|
+
.string()
|
|
12
|
+
.regex(/^[A-Z]{1,2}\d[A-Z\d]?\d[A-Z]{2}$/u)
|
|
13
|
+
.nonoptional());
|
|
14
|
+
return createRule({ validator });
|
|
15
|
+
}
|
|
16
|
+
export default getPostcodeRule;
|
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
export declare const addressTable: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
2
|
+
name: "addresses";
|
|
3
|
+
schema: undefined;
|
|
4
|
+
columns: {
|
|
5
|
+
id: import("drizzle-orm/pg-core").PgColumn<{
|
|
6
|
+
name: "id";
|
|
7
|
+
tableName: "addresses";
|
|
8
|
+
dataType: "string";
|
|
9
|
+
columnType: "PgText";
|
|
10
|
+
data: `add_${string}`;
|
|
11
|
+
driverParam: string;
|
|
12
|
+
notNull: true;
|
|
13
|
+
hasDefault: false;
|
|
14
|
+
isPrimaryKey: true;
|
|
15
|
+
isAutoincrement: false;
|
|
16
|
+
hasRuntimeDefault: false;
|
|
17
|
+
enumValues: [string, ...string[]];
|
|
18
|
+
baseColumn: never;
|
|
19
|
+
identity: undefined;
|
|
20
|
+
generated: undefined;
|
|
21
|
+
}, {}, {
|
|
22
|
+
$type: `add_${string}`;
|
|
23
|
+
}>;
|
|
24
|
+
subjectID: import("drizzle-orm/pg-core").PgColumn<{
|
|
25
|
+
name: "subjectID";
|
|
26
|
+
tableName: "addresses";
|
|
27
|
+
dataType: "string";
|
|
28
|
+
columnType: "PgText";
|
|
29
|
+
data: string;
|
|
30
|
+
driverParam: string;
|
|
31
|
+
notNull: true;
|
|
32
|
+
hasDefault: false;
|
|
33
|
+
isPrimaryKey: false;
|
|
34
|
+
isAutoincrement: false;
|
|
35
|
+
hasRuntimeDefault: false;
|
|
36
|
+
enumValues: [string, ...string[]];
|
|
37
|
+
baseColumn: never;
|
|
38
|
+
identity: undefined;
|
|
39
|
+
generated: undefined;
|
|
40
|
+
}, {}, {}>;
|
|
41
|
+
line1: import("drizzle-orm/pg-core").PgColumn<{
|
|
42
|
+
name: "line1";
|
|
43
|
+
tableName: "addresses";
|
|
44
|
+
dataType: "string";
|
|
45
|
+
columnType: "PgText";
|
|
46
|
+
data: string;
|
|
47
|
+
driverParam: string;
|
|
48
|
+
notNull: true;
|
|
49
|
+
hasDefault: false;
|
|
50
|
+
isPrimaryKey: false;
|
|
51
|
+
isAutoincrement: false;
|
|
52
|
+
hasRuntimeDefault: false;
|
|
53
|
+
enumValues: [string, ...string[]];
|
|
54
|
+
baseColumn: never;
|
|
55
|
+
identity: undefined;
|
|
56
|
+
generated: undefined;
|
|
57
|
+
}, {}, {}>;
|
|
58
|
+
line2: import("drizzle-orm/pg-core").PgColumn<{
|
|
59
|
+
name: "line2";
|
|
60
|
+
tableName: "addresses";
|
|
61
|
+
dataType: "string";
|
|
62
|
+
columnType: "PgText";
|
|
63
|
+
data: string;
|
|
64
|
+
driverParam: string;
|
|
65
|
+
notNull: false;
|
|
66
|
+
hasDefault: false;
|
|
67
|
+
isPrimaryKey: false;
|
|
68
|
+
isAutoincrement: false;
|
|
69
|
+
hasRuntimeDefault: false;
|
|
70
|
+
enumValues: [string, ...string[]];
|
|
71
|
+
baseColumn: never;
|
|
72
|
+
identity: undefined;
|
|
73
|
+
generated: undefined;
|
|
74
|
+
}, {}, {}>;
|
|
75
|
+
city: import("drizzle-orm/pg-core").PgColumn<{
|
|
76
|
+
name: "city";
|
|
77
|
+
tableName: "addresses";
|
|
78
|
+
dataType: "string";
|
|
79
|
+
columnType: "PgText";
|
|
80
|
+
data: string;
|
|
81
|
+
driverParam: string;
|
|
82
|
+
notNull: true;
|
|
83
|
+
hasDefault: false;
|
|
84
|
+
isPrimaryKey: false;
|
|
85
|
+
isAutoincrement: false;
|
|
86
|
+
hasRuntimeDefault: false;
|
|
87
|
+
enumValues: [string, ...string[]];
|
|
88
|
+
baseColumn: never;
|
|
89
|
+
identity: undefined;
|
|
90
|
+
generated: undefined;
|
|
91
|
+
}, {}, {}>;
|
|
92
|
+
county: import("drizzle-orm/pg-core").PgColumn<{
|
|
93
|
+
name: "county";
|
|
94
|
+
tableName: "addresses";
|
|
95
|
+
dataType: "string";
|
|
96
|
+
columnType: "PgText";
|
|
97
|
+
data: string;
|
|
98
|
+
driverParam: string;
|
|
99
|
+
notNull: true;
|
|
100
|
+
hasDefault: false;
|
|
101
|
+
isPrimaryKey: false;
|
|
102
|
+
isAutoincrement: false;
|
|
103
|
+
hasRuntimeDefault: false;
|
|
104
|
+
enumValues: [string, ...string[]];
|
|
105
|
+
baseColumn: never;
|
|
106
|
+
identity: undefined;
|
|
107
|
+
generated: undefined;
|
|
108
|
+
}, {}, {}>;
|
|
109
|
+
postcode: import("drizzle-orm/pg-core").PgColumn<{
|
|
110
|
+
name: "postcode";
|
|
111
|
+
tableName: "addresses";
|
|
112
|
+
dataType: "string";
|
|
113
|
+
columnType: "PgText";
|
|
114
|
+
data: string;
|
|
115
|
+
driverParam: string;
|
|
116
|
+
notNull: true;
|
|
117
|
+
hasDefault: false;
|
|
118
|
+
isPrimaryKey: false;
|
|
119
|
+
isAutoincrement: false;
|
|
120
|
+
hasRuntimeDefault: false;
|
|
121
|
+
enumValues: [string, ...string[]];
|
|
122
|
+
baseColumn: never;
|
|
123
|
+
identity: undefined;
|
|
124
|
+
generated: undefined;
|
|
125
|
+
}, {}, {}>;
|
|
126
|
+
createdAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
127
|
+
name: "createdAt";
|
|
128
|
+
tableName: "addresses";
|
|
129
|
+
dataType: "date";
|
|
130
|
+
columnType: "PgTimestamp";
|
|
131
|
+
data: Date;
|
|
132
|
+
driverParam: string;
|
|
133
|
+
notNull: true;
|
|
134
|
+
hasDefault: true;
|
|
135
|
+
isPrimaryKey: false;
|
|
136
|
+
isAutoincrement: false;
|
|
137
|
+
hasRuntimeDefault: false;
|
|
138
|
+
enumValues: undefined;
|
|
139
|
+
baseColumn: never;
|
|
140
|
+
identity: undefined;
|
|
141
|
+
generated: undefined;
|
|
142
|
+
}, {}, {}>;
|
|
143
|
+
deletedAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
144
|
+
name: "deletedAt";
|
|
145
|
+
tableName: "addresses";
|
|
146
|
+
dataType: "date";
|
|
147
|
+
columnType: "PgTimestamp";
|
|
148
|
+
data: Date;
|
|
149
|
+
driverParam: string;
|
|
150
|
+
notNull: false;
|
|
151
|
+
hasDefault: false;
|
|
152
|
+
isPrimaryKey: false;
|
|
153
|
+
isAutoincrement: false;
|
|
154
|
+
hasRuntimeDefault: false;
|
|
155
|
+
enumValues: undefined;
|
|
156
|
+
baseColumn: never;
|
|
157
|
+
identity: undefined;
|
|
158
|
+
generated: undefined;
|
|
159
|
+
}, {}, {}>;
|
|
160
|
+
};
|
|
161
|
+
dialect: "pg";
|
|
162
|
+
}>;
|
|
163
|
+
export type AddressTable = typeof addressTable.$inferSelect;
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { sql } from 'drizzle-orm';
|
|
2
|
+
import { pgTable, text, timestamp } from 'drizzle-orm/pg-core';
|
|
3
|
+
export const addressTable = pgTable('addresses', {
|
|
4
|
+
id: text('id').$type().primaryKey(),
|
|
5
|
+
subjectID: text('subjectID').notNull(),
|
|
6
|
+
line1: text('line1').notNull(),
|
|
7
|
+
line2: text('line2'),
|
|
8
|
+
city: text('city').notNull(),
|
|
9
|
+
county: text('county').notNull(),
|
|
10
|
+
postcode: text('postcode').notNull(),
|
|
11
|
+
createdAt: timestamp('createdAt')
|
|
12
|
+
.notNull()
|
|
13
|
+
.default(sql `CURRENT_TIMESTAMP`),
|
|
14
|
+
deletedAt: timestamp('deletedAt'),
|
|
15
|
+
});
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
package/dist/ui.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as AddressForm } from './forms/AddressForm';
|
package/dist/ui.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as AddressForm } from './forms/AddressForm';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const ADDRESS_PREFIX = 'add';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { MakeNullOptional } from '@sqrzro/server/utility';
|
|
2
|
+
import type { AddressTable } from '../schema';
|
|
3
|
+
import type { AddressID } from './ids';
|
|
4
|
+
export type Address = AddressTable;
|
|
5
|
+
export type AddressDataWithID = MakeNullOptional<Omit<Address, 'createdAt' | 'deletedAt'>>;
|
|
6
|
+
export type AddressData = Omit<AddressDataWithID, 'id'>;
|
|
7
|
+
export interface AddressFilters {
|
|
8
|
+
id: AddressID[];
|
|
9
|
+
subjectID: string | string[];
|
|
10
|
+
}
|
|
11
|
+
export type WithAddress<T> = T & {
|
|
12
|
+
address: Address | null;
|
|
13
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sqrzro/addon-addresses",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "4.0.0-alpha.1",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
"./interfaces": {
|
|
9
|
+
"default": "./dist/interfaces.js",
|
|
10
|
+
"types": "./dist/interfaces.d.ts"
|
|
11
|
+
},
|
|
12
|
+
"./schema": {
|
|
13
|
+
"default": "./dist/schema.js",
|
|
14
|
+
"types": "./dist/schema.d.ts"
|
|
15
|
+
},
|
|
16
|
+
"./server": {
|
|
17
|
+
"default": "./dist/server.js",
|
|
18
|
+
"types": "./dist/server.d.ts"
|
|
19
|
+
},
|
|
20
|
+
"./ui": {
|
|
21
|
+
"default": "./dist/ui.js",
|
|
22
|
+
"types": "./dist/ui.d.ts"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"drizzle-orm": "^0.45.1",
|
|
27
|
+
"zod": "^4.3.6",
|
|
28
|
+
"@sqrzro/server": "^4.0.0-alpha.62",
|
|
29
|
+
"@sqrzro/ui": "^4.0.0-alpha.76",
|
|
30
|
+
"@sqrzro/utility": "^4.0.0-alpha.22"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/react": "^19.2.14",
|
|
34
|
+
"typescript": "^5.9.3",
|
|
35
|
+
"szx": "^4.0.0-alpha.18"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"next": "^16.1.6",
|
|
39
|
+
"react": "^19.2.4"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsc",
|
|
43
|
+
"dev": "tsc --watch",
|
|
44
|
+
"start": "pnpm dev",
|
|
45
|
+
"szx": "szx"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Default, EditableForm, EditableTextFormField, useEditableForm } from '@sqrzro/ui/forms';
|
|
4
|
+
import { formatPostcode } from '@sqrzro/utility';
|
|
5
|
+
|
|
6
|
+
import submit from './server';
|
|
7
|
+
import type { AddressFormFields } from './interfaces';
|
|
8
|
+
|
|
9
|
+
interface AddressFormProps {
|
|
10
|
+
defaults?: Default<AddressFormFields> | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function AddressForm({ defaults }: Readonly<AddressFormProps>): React.ReactElement {
|
|
14
|
+
const { fieldProps, formProps } = useEditableForm({
|
|
15
|
+
defaults: defaults ?? {},
|
|
16
|
+
onSubmit: submit,
|
|
17
|
+
title: 'Address',
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<EditableForm {...formProps}>
|
|
22
|
+
<EditableTextFormField {...fieldProps('line1', 'Line 1')} />
|
|
23
|
+
<EditableTextFormField {...fieldProps('line2', 'Line 2')} />
|
|
24
|
+
<EditableTextFormField {...fieldProps('city')} />
|
|
25
|
+
<EditableTextFormField {...fieldProps('county')} />
|
|
26
|
+
<EditableTextFormField {...fieldProps('postcode')} renderValue={formatPostcode} />
|
|
27
|
+
</EditableForm>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default AddressForm;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use server';
|
|
2
|
+
|
|
3
|
+
import { FormResponse, submitForm } from '@sqrzro/server/forms';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
import AddressRepository from '../../repositories/AddressRepository';
|
|
7
|
+
import getPostcodeRule from '../../rules/postcode';
|
|
8
|
+
import type { Address } from '../../utility/interfaces';
|
|
9
|
+
|
|
10
|
+
import type { AddressFormFields } from './interfaces';
|
|
11
|
+
|
|
12
|
+
const schema = z.object({
|
|
13
|
+
city: z.string().min(4).max(20).nonoptional(),
|
|
14
|
+
county: z.string().min(4).max(20).nonoptional(),
|
|
15
|
+
id: AddressRepository.getIDRule('id').validator.optional(),
|
|
16
|
+
line1: z.string().min(4).max(30).nonoptional(),
|
|
17
|
+
line2: z.string().max(30).nullable().optional(),
|
|
18
|
+
subjectID: z.string().nonoptional(),
|
|
19
|
+
postcode: getPostcodeRule().validator.nonoptional(),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
async function fn(data: z.infer<typeof schema>): Promise<Address> {
|
|
23
|
+
const { id, ...rest } = data;
|
|
24
|
+
|
|
25
|
+
if (typeof id !== 'undefined') {
|
|
26
|
+
return AddressRepository.update({ id, ...rest });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return AddressRepository.create(rest);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function submit(formData: AddressFormFields): FormResponse<Address> {
|
|
33
|
+
return submitForm({
|
|
34
|
+
fn,
|
|
35
|
+
formData,
|
|
36
|
+
schema,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default submit;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { db } from '@sqrzro/server/database';
|
|
2
|
+
import { createRepository } from '@sqrzro/server/database';
|
|
3
|
+
import type { QueryConditions, QueryConfig } from '@sqrzro/server/database';
|
|
4
|
+
import { generateID } from '@sqrzro/server/utility';
|
|
5
|
+
import { pick } from '@sqrzro/utility';
|
|
6
|
+
import { and, eq, inArray } from 'drizzle-orm';
|
|
7
|
+
|
|
8
|
+
import { ADDRESS_PREFIX, AddressID } from '../utility/ids';
|
|
9
|
+
import type { Address, AddressData, AddressFilters } from '../utility/interfaces';
|
|
10
|
+
|
|
11
|
+
import { addressTable } from '../schema';
|
|
12
|
+
|
|
13
|
+
const fillable: (keyof AddressData)[] = [
|
|
14
|
+
'city',
|
|
15
|
+
'county',
|
|
16
|
+
'line1',
|
|
17
|
+
'line2',
|
|
18
|
+
'postcode',
|
|
19
|
+
'subjectID',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const AddressRepository = createRepository({
|
|
23
|
+
async applyFilters(filters: Partial<AddressFilters> | null): Promise<QueryConditions> {
|
|
24
|
+
const conditions: QueryConditions = [];
|
|
25
|
+
|
|
26
|
+
if (filters?.id) {
|
|
27
|
+
conditions.push(inArray(addressTable.id, filters.id));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (filters?.subjectID) {
|
|
31
|
+
if (Array.isArray(filters.subjectID)) {
|
|
32
|
+
conditions.push(inArray(addressTable.subjectID, filters.subjectID));
|
|
33
|
+
} else {
|
|
34
|
+
conditions.push(eq(addressTable.subjectID, filters.subjectID));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return conditions;
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
indexes: { id: addressTable.id, subjectID: addressTable.subjectID },
|
|
42
|
+
|
|
43
|
+
async makeQuery(conditions: QueryConditions, config: QueryConfig): Promise<Address[]> {
|
|
44
|
+
return db
|
|
45
|
+
.select()
|
|
46
|
+
.from(addressTable)
|
|
47
|
+
.where(and(...conditions))
|
|
48
|
+
.limit(config.limit)
|
|
49
|
+
.offset(config.offset);
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
async makeDeleteMutation(id: AddressID): Promise<void> {
|
|
53
|
+
await db.update(addressTable).set({ deletedAt: new Date() }).where(eq(addressTable.id, id));
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
async makeUndeleteMutation(id: AddressID): Promise<void> {
|
|
57
|
+
await db.update(addressTable).set({ deletedAt: null }).where(eq(addressTable.id, id));
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
async makeInsertMutation(data: AddressData): Promise<Address> {
|
|
61
|
+
const values = {
|
|
62
|
+
id: generateID<AddressID>(ADDRESS_PREFIX),
|
|
63
|
+
...pick(data, fillable),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const [result] = await db.insert(addressTable).values(values).returning();
|
|
67
|
+
return result;
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
async makeUpdateMutation(data: Partial<AddressData> & { id: AddressID }): Promise<Address> {
|
|
71
|
+
const [result] = await db
|
|
72
|
+
.update(addressTable)
|
|
73
|
+
.set(data)
|
|
74
|
+
.where(eq(addressTable.id, data.id))
|
|
75
|
+
.returning();
|
|
76
|
+
|
|
77
|
+
return result;
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
paramKey: 'address',
|
|
81
|
+
prefix: ADDRESS_PREFIX,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
function ensureBySubjectID(subjectID: string): Promise<Address> {
|
|
85
|
+
return AddressRepository.ensureByIndex('subjectID', subjectID);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getBySubjectID(subjectID: string): Promise<Address | null> {
|
|
89
|
+
return AddressRepository.getByIndex('subjectID', subjectID);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export default { ...AddressRepository, ensureBySubjectID, getBySubjectID };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { RepositoryConfig, UnwrapModel } from '@sqrzro/server/database';
|
|
2
|
+
|
|
3
|
+
import AddressRepository from '../repositories/AddressRepository';
|
|
4
|
+
|
|
5
|
+
import { AddressID } from '../utility/ids';
|
|
6
|
+
import { Address, AddressData, WithAddress } from '../utility/interfaces';
|
|
7
|
+
|
|
8
|
+
interface AddressesRepositoryConfig {}
|
|
9
|
+
|
|
10
|
+
export function withAddressesConfig<T extends RepositoryConfig>(cfg: T) {
|
|
11
|
+
return {
|
|
12
|
+
...cfg,
|
|
13
|
+
makeQuery: async (
|
|
14
|
+
...args: Parameters<NonNullable<typeof cfg.makeQuery>>
|
|
15
|
+
): Promise<WithAddress<UnwrapModel<T>>[]> => {
|
|
16
|
+
const results = await cfg.makeQuery?.(...args);
|
|
17
|
+
const ids = results?.map((item: any) => item.id) ?? [];
|
|
18
|
+
|
|
19
|
+
const addresses = await AddressRepository.getAll({ filters: { subjectID: ids } });
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
results?.map((item: any) => ({
|
|
23
|
+
...item,
|
|
24
|
+
address: addresses.find((a) => a.subjectID === item.id) ?? null,
|
|
25
|
+
})) ?? []
|
|
26
|
+
);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function withAddresses(config: AddressesRepositoryConfig) {
|
|
32
|
+
return function <
|
|
33
|
+
TRepo extends { ensureByID: (...args: any[]) => any; update: (...args: any[]) => any },
|
|
34
|
+
>(repository: TRepo) {
|
|
35
|
+
async function attachAddress(subjectID: string, data: AddressData): Promise<Address> {
|
|
36
|
+
const model = (await repository.ensureByID(subjectID)) as {
|
|
37
|
+
addressID: AddressID | null;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
if (model.addressID) {
|
|
41
|
+
return AddressRepository.update({ ...data, id: model.addressID });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const address = await AddressRepository.create(data);
|
|
45
|
+
await repository.update({ id: subjectID, addressID: address.id });
|
|
46
|
+
|
|
47
|
+
return address;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function withAddress<T extends { id: string }>(subject: T): Promise<WithAddress<T>> {
|
|
51
|
+
const [address] = await AddressRepository.getAll({
|
|
52
|
+
filters: { subjectID: subject.id },
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
...subject,
|
|
57
|
+
address: address ?? null,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
...repository,
|
|
63
|
+
attachAddress,
|
|
64
|
+
withAddress,
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createRule } from '@sqrzro/server/forms';
|
|
2
|
+
import z from 'zod';
|
|
3
|
+
|
|
4
|
+
function preprocess(value: unknown): unknown {
|
|
5
|
+
if (typeof value === 'string') {
|
|
6
|
+
return value.replace(/[\s-]/g, '').toUpperCase();
|
|
7
|
+
}
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function getPostcodeRule() {
|
|
12
|
+
const validator = z.preprocess(
|
|
13
|
+
preprocess,
|
|
14
|
+
z
|
|
15
|
+
.string()
|
|
16
|
+
.regex(/^[A-Z]{1,2}\d[A-Z\d]?\d[A-Z]{2}$/u)
|
|
17
|
+
.nonoptional()
|
|
18
|
+
);
|
|
19
|
+
return createRule({ validator });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default getPostcodeRule;
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { sql } from 'drizzle-orm';
|
|
2
|
+
import { pgTable, text, timestamp } from 'drizzle-orm/pg-core';
|
|
3
|
+
|
|
4
|
+
import type { AddressID } from './utility/ids';
|
|
5
|
+
|
|
6
|
+
export const addressTable = pgTable('addresses', {
|
|
7
|
+
id: text('id').$type<AddressID>().primaryKey(),
|
|
8
|
+
subjectID: text('subjectID').notNull(),
|
|
9
|
+
line1: text('line1').notNull(),
|
|
10
|
+
line2: text('line2'),
|
|
11
|
+
city: text('city').notNull(),
|
|
12
|
+
county: text('county').notNull(),
|
|
13
|
+
postcode: text('postcode').notNull(),
|
|
14
|
+
createdAt: timestamp('createdAt')
|
|
15
|
+
.notNull()
|
|
16
|
+
.default(sql`CURRENT_TIMESTAMP`),
|
|
17
|
+
deletedAt: timestamp('deletedAt'),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export type AddressTable = typeof addressTable.$inferSelect;
|
package/src/server.ts
ADDED
package/src/ui.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as AddressForm } from './forms/AddressForm';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { MakeNullOptional } from '@sqrzro/server/utility';
|
|
2
|
+
|
|
3
|
+
import type { AddressTable } from '../schema';
|
|
4
|
+
|
|
5
|
+
import type { AddressID } from './ids';
|
|
6
|
+
|
|
7
|
+
export type Address = AddressTable;
|
|
8
|
+
export type AddressDataWithID = MakeNullOptional<Omit<Address, 'createdAt' | 'deletedAt'>>;
|
|
9
|
+
export type AddressData = Omit<AddressDataWithID, 'id'>;
|
|
10
|
+
|
|
11
|
+
export interface AddressFilters {
|
|
12
|
+
id: AddressID[];
|
|
13
|
+
subjectID: string | string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type WithAddress<T> = T & { address: Address | null };
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"allowSyntheticDefaultImports": true,
|
|
4
|
+
"declaration": true,
|
|
5
|
+
"esModuleInterop": true,
|
|
6
|
+
"importHelpers": true,
|
|
7
|
+
"lib": ["ESNext", "DOM"],
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"module": "ESNext",
|
|
10
|
+
"moduleResolution": "bundler",
|
|
11
|
+
"outDir": "dist",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"rootDir": "src",
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"strict": true,
|
|
16
|
+
"target": "esnext"
|
|
17
|
+
},
|
|
18
|
+
"include": ["**/*.ts", "**/*.tsx"],
|
|
19
|
+
"exclude": ["dist", "./*.d.ts", "node_modules"]
|
|
20
|
+
}
|