diff --git a/packages/admin/package-lock.json b/packages/admin/package-lock.json
index 524f14a6..cb350c4d 100644
--- a/packages/admin/package-lock.json
+++ b/packages/admin/package-lock.json
@@ -9,6 +9,7 @@
"version": "0.0.0",
"dependencies": {
"ra-data-json-server": "^5.15.0",
+ "ra-data-simple-rest": "^5.15.0",
"react": "^19.2.8",
"react-admin": "^5.15.1",
"react-dom": "^19.2.8"
@@ -59,7 +60,6 @@
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.7",
"@babel/generator": "^7.29.7",
@@ -333,7 +333,6 @@
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -377,7 +376,6 @@
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz",
"integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -678,7 +676,6 @@
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-9.3.1.tgz",
"integrity": "sha512-rZj5ccG7vkpV38o/l4ys+chfE9GFypmvZr9dSNBoYVCNBD9yC6KfKq1TYpEMshWbjic7GKQ8MA1LQlcpGgcq9Q==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/runtime": "^7.29.7"
},
@@ -705,7 +702,6 @@
"resolved": "https://registry.npmjs.org/@mui/material/-/material-9.3.1.tgz",
"integrity": "sha512-NahAEGIXqS1K0bA4th1jeFxBguS59NOcLbMA0vU+fSaPWKjtwGBGGHeTwlc9PSmzMjOZKceeFWESB8fVHr31hA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/runtime": "^7.29.7",
"@mui/core-downloads-tracker": "^9.3.1",
@@ -816,7 +812,6 @@
"resolved": "https://registry.npmjs.org/@mui/system/-/system-9.3.0.tgz",
"integrity": "sha512-0l4LqHJxZj65xSrioniGsxm7VNoGXonPo203oZjhBUvIDPeBqRTb7Mqc45Qxs6sO6WGR7WE/9cJ6lb4lkvHkjg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/runtime": "^7.29.7",
"@mui/private-theming": "^9.3.0",
@@ -874,7 +869,6 @@
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-9.3.0.tgz",
"integrity": "sha512-2HZdHwWJ6eB+7lVGSOHsByGw8jeRulT4g0NZ608Wb8Q57DE2jbNqrWPFuJsvkQQiBiTmlpvQL3i+/62zsiPrkw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/runtime": "^7.29.7",
"@mui/types": "^9.3.0",
@@ -1442,7 +1436,6 @@
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.4.tgz",
"integrity": "sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@tanstack/query-core": "5.101.4"
},
@@ -1481,7 +1474,6 @@
"integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"undici-types": "~8.3.0"
}
@@ -1503,7 +1495,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz",
"integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==",
"license": "MIT",
- "peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -1579,7 +1570,6 @@
"integrity": "sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.67.0",
"@typescript-eslint/types": "8.67.0",
@@ -1801,7 +1791,6 @@
"integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -1925,7 +1914,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.11.12",
"caniuse-lite": "^1.0.30001809",
@@ -2096,8 +2084,7 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/date-fns": {
"version": "3.6.0",
@@ -2299,7 +2286,6 @@
"integrity": "sha512-wqA7W2jbsC/BnV9Iv1UZpKVFkO1AdNoSmYW8NWG4HNOBbkAMvIqDZ27pI2f07dqn583NcIC44ckjAcOXDL1QbQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"workspaces": [
"packages/*"
],
@@ -3518,7 +3504,6 @@
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -3631,7 +3616,6 @@
"resolved": "https://registry.npmjs.org/ra-core/-/ra-core-5.15.0.tgz",
"integrity": "sha512-8PP+ulxmEPW/p9V4j0kwydD1yLYdApzYcfOVXY6MDezSd/1QzJKdIHO5WrXWThlCMId/nUpmBrtCHLCYYZ9t5A==",
"license": "MIT",
- "peer": true,
"dependencies": {
"date-fns": "^3.6.0",
"eventemitter3": "^5.0.1",
@@ -3661,6 +3645,18 @@
"ra-core": "^5.15.0"
}
},
+ "node_modules/ra-data-simple-rest": {
+ "version": "5.15.0",
+ "resolved": "https://registry.npmjs.org/ra-data-simple-rest/-/ra-data-simple-rest-5.15.0.tgz",
+ "integrity": "sha512-C7WwkUX+xAXPsCiA01cn6SwxNS7eLugwlw7iT8VyoENpQp6Q66nhT6ZXtwh2SOLFdjBk5Tyl6lKIEtrgFkEGgA==",
+ "license": "MIT",
+ "dependencies": {
+ "query-string": "^7.1.3"
+ },
+ "peerDependencies": {
+ "ra-core": "^5.0.0"
+ }
+ },
"node_modules/ra-i18n-polyglot": {
"version": "5.15.0",
"resolved": "https://registry.npmjs.org/ra-i18n-polyglot/-/ra-i18n-polyglot-5.15.0.tgz",
@@ -3721,7 +3717,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
"integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -3755,7 +3750,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
"integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -3797,7 +3791,6 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.85.0.tgz",
"integrity": "sha512-U2MTriFXnclmV4rOE20p2DcRFv5WEg3FIcBFOKcOLFHDVvGIMPvLTkTWefUsonmlaVy23khVDxDWym6uJVGOzw==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=18.0.0"
},
@@ -3826,15 +3819,13 @@
"version": "19.2.8",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz",
"integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/react-router": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz",
"integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
@@ -3857,7 +3848,6 @@
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.2.tgz",
"integrity": "sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"react-router": "7.18.2"
},
@@ -4126,7 +4116,6 @@
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
"dev": true,
"license": "Apache-2.0",
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -4213,7 +4202,6 @@
"integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"lightningcss": "^1.33.0",
"picomatch": "^4.0.5",
@@ -4347,7 +4335,6 @@
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
diff --git a/packages/admin/package.json b/packages/admin/package.json
index 16ebefcf..e817b99c 100644
--- a/packages/admin/package.json
+++ b/packages/admin/package.json
@@ -14,6 +14,7 @@
},
"dependencies": {
"ra-data-json-server": "^5.15.0",
+ "ra-data-simple-rest": "^5.15.0",
"react": "^19.2.8",
"react-admin": "^5.15.1",
"react-dom": "^19.2.8"
diff --git a/packages/admin/src/App.tsx b/packages/admin/src/App.tsx
index 53d44e5a..104f385d 100644
--- a/packages/admin/src/App.tsx
+++ b/packages/admin/src/App.tsx
@@ -1,15 +1,28 @@
-import { Admin, Resource, ListGuesser } from 'react-admin';
-import jsonServerProvider from 'ra-data-json-server';
+import { Admin, Resource } from 'react-admin';
+import { ConfigProvider } from './contexts/Config.provider';
+import { useConfig } from './contexts/Config.context';
+import { createMongoIdDataProvider } from './dataProviders/MongoIdDataProvider';
+import { ListProjects } from './pages/projects/ListProjects.page';
+import { ShowProject } from './pages/projects/ShowProject.page';
+import { CreateProject } from './pages/projects/CreateProject.page';
-const dataProvider = jsonServerProvider('https://jsonplaceholder.typicode.com');
+const App: React.FC = () => {
+ return (
+
+
+
+ );
+};
-function App() {
+const ConfigWrappedInner: React.FC = () => {
+ // Setup React Admin data provider
+ const config = useConfig();
+ const dataProvider = createMongoIdDataProvider(config.backend.apiBase);
return (
-
-
+
);
-}
+};
export default App;
diff --git a/packages/admin/src/contexts/Config.context.tsx b/packages/admin/src/contexts/Config.context.tsx
new file mode 100644
index 00000000..72913ad0
--- /dev/null
+++ b/packages/admin/src/contexts/Config.context.tsx
@@ -0,0 +1,14 @@
+import { createContext, useContext } from 'react';
+
+export interface ConfigFields {
+ auth: {
+ authorityURL: string;
+ clientID: string;
+ };
+ backend: {
+ apiBase: string;
+ };
+}
+
+export const ConfigContext = createContext({} as ConfigFields);
+export const useConfig = () => useContext(ConfigContext);
diff --git a/packages/admin/src/contexts/Config.provider.tsx b/packages/admin/src/contexts/Config.provider.tsx
new file mode 100644
index 00000000..a2c8ed87
--- /dev/null
+++ b/packages/admin/src/contexts/Config.provider.tsx
@@ -0,0 +1,16 @@
+import { type ReactNode } from 'react';
+import { ConfigContext, type ConfigFields } from './Config.context';
+
+const devValues: ConfigFields = {
+ auth: {
+ authorityURL: import.meta.env.VITE_AUTHORITY_URL,
+ clientID: import.meta.env.VITE_AUTH_CLIENT_ID
+ },
+ backend: {
+ apiBase: import.meta.env.VITE_BACKEND_API_BASE_URL
+ }
+};
+
+export const ConfigProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
+ return {children};
+};
diff --git a/packages/admin/src/dataProviders/MongoIdDataProvider.ts b/packages/admin/src/dataProviders/MongoIdDataProvider.ts
new file mode 100644
index 00000000..34e27ede
--- /dev/null
+++ b/packages/admin/src/dataProviders/MongoIdDataProvider.ts
@@ -0,0 +1,70 @@
+import type {
+ CreateParams,
+ DataProvider,
+ DeleteParams,
+ GetListParams,
+ GetManyParams,
+ GetManyReferenceParams,
+ GetOneParams,
+ Identifier,
+ QueryFunctionContext,
+ RaRecord,
+ UpdateParams,
+} from 'react-admin';
+import simpleRestProvider from 'ra-data-simple-rest';
+
+type MongoRecord = RaRecord & { _id: Identifier };
+
+// Records coming back from the API use Mongo's "_id" as their primary key.
+// React Admin requires every record to have an "id" field, so this maps
+// "_id" -> "id" on every response coming out of the underlying REST provider.
+// See https://marmelab.com/react-admin/FAQ.html#can-i-have-custom-identifiersprimary-keys-for-my-resources
+const toRaRecord = (record: MongoRecord): RaRecord => ({
+ ...record,
+ id: record._id,
+});
+
+export const createMongoIdDataProvider = (apiBase: string): DataProvider => {
+ const baseProvider = simpleRestProvider(apiBase);
+
+ const provider = {
+ ...baseProvider,
+ getList: (resource: string, params: GetListParams & QueryFunctionContext) =>
+ baseProvider.getList(resource, params).then((response) => ({
+ ...response,
+ data: response.data.map(toRaRecord),
+ })),
+ getOne: (resource: string, params: GetOneParams & QueryFunctionContext) =>
+ baseProvider.getOne(resource, params).then((response) => ({
+ ...response,
+ data: toRaRecord(response.data),
+ })),
+ getMany: (resource: string, params: GetManyParams & QueryFunctionContext) =>
+ baseProvider.getMany(resource, params).then((response) => ({
+ ...response,
+ data: response.data.map(toRaRecord),
+ })),
+ getManyReference: (resource: string, params: GetManyReferenceParams & QueryFunctionContext) =>
+ baseProvider.getManyReference(resource, params).then((response) => ({
+ ...response,
+ data: response.data.map(toRaRecord),
+ })),
+ create: (resource: string, params: CreateParams) =>
+ baseProvider.create(resource, params).then((response) => ({
+ ...response,
+ data: toRaRecord(response.data),
+ })),
+ update: (resource: string, params: UpdateParams) =>
+ baseProvider.update(resource, params).then((response) => ({
+ ...response,
+ data: toRaRecord(response.data),
+ })),
+ delete: (resource: string, params: DeleteParams) =>
+ baseProvider.delete(resource, params).then((response) => ({
+ ...response,
+ data: toRaRecord(response.data),
+ })),
+ };
+
+ return provider as unknown as DataProvider;
+};
diff --git a/packages/admin/src/pages/projects/CreateProject.page.tsx b/packages/admin/src/pages/projects/CreateProject.page.tsx
new file mode 100644
index 00000000..f178d342
--- /dev/null
+++ b/packages/admin/src/pages/projects/CreateProject.page.tsx
@@ -0,0 +1,12 @@
+import { Create, required, SimpleForm, TextInput } from 'react-admin';
+
+export const CreateProject: React.FC = () => {
+ return (
+
+
+
+
+
+
+ );
+};
diff --git a/packages/admin/src/pages/projects/ListProjects.page.tsx b/packages/admin/src/pages/projects/ListProjects.page.tsx
new file mode 100644
index 00000000..8c354934
--- /dev/null
+++ b/packages/admin/src/pages/projects/ListProjects.page.tsx
@@ -0,0 +1,12 @@
+import { Datagrid, List, TextField } from 'react-admin';
+
+export const ListProjects: React.FC = () => {
+ return (
+
+
+
+
+
+
+ );
+};
diff --git a/packages/admin/src/pages/projects/ShowProject.page.tsx b/packages/admin/src/pages/projects/ShowProject.page.tsx
new file mode 100644
index 00000000..035a1c80
--- /dev/null
+++ b/packages/admin/src/pages/projects/ShowProject.page.tsx
@@ -0,0 +1,12 @@
+import { Show, SimpleShowLayout, TextField } from 'react-admin';
+
+export const ShowProject: React.FC = () => {
+ return (
+
+
+
+
+
+
+ );
+};
diff --git a/packages/server/package-lock.json b/packages/server/package-lock.json
index 0d13d108..07f8bf23 100644
--- a/packages/server/package-lock.json
+++ b/packages/server/package-lock.json
@@ -12,6 +12,7 @@
"@apollo/server": "^5.5.1",
"@apollo/subgraph": "^2.14.3",
"@as-integrations/express5": "^1.1.2",
+ "@bu-sail/ra-query-core": "^0.1.2",
"@google-cloud/run": "^4.0.0",
"@google-cloud/secret-manager": "^7.0.0",
"@google-cloud/storage": "^7.22.0",
@@ -31,6 +32,8 @@
"@types/passport-jwt": "^4.0.1",
"casbin": "^5.51.1",
"casbin-mongoose-adapter": "^5.6.1",
+ "class-transformer": "^0.5.1",
+ "class-validator": "^0.15.1",
"csv-parser": "^3.2.1",
"firebase-admin": "^14.2.0",
"graphql-request": "^7.4.0",
@@ -286,7 +289,6 @@
"resolved": "https://registry.npmjs.org/@apollo/server/-/server-5.5.1.tgz",
"integrity": "sha512-Rn3g5TJQsMSUY23CWZTghWdBWyjX7dP1eaEBPkvmM2RHi82cDcpgTIkSCbGvtTUEGjwopLv1AAooU/n7iIZ20A==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@apollo/cache-control-types": "^1.0.3",
"@apollo/server-gateway-interface": "^2.0.0",
@@ -361,7 +363,6 @@
"resolved": "https://registry.npmjs.org/@apollo/subgraph/-/subgraph-2.14.3.tgz",
"integrity": "sha512-ElfvdJJFKUQn32d7zUkBaqWhco5nSknlZKp2u0Z1FdDb5PGWfXUxuVQRpCv9zfqXXwKfCWithN6uchkDzy+khQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@apollo/cache-control-types": "^1.0.2",
"@apollo/federation-internals": "2.14.3"
@@ -597,7 +598,6 @@
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.7",
"@babel/generator": "^7.29.7",
@@ -1096,6 +1096,12 @@
"url": "https://github.com/sponsors/Borewit"
}
},
+ "node_modules/@bu-sail/ra-query-core": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/@bu-sail/ra-query-core/-/ra-query-core-0.1.2.tgz",
+ "integrity": "sha512-bYeMWakIYkRwcS/KUjN2hlx6exNdW9oCmBEYi2zHSkZYdDprcEG6MnXJLs+zFYTTHUDb3QR9Ex8gXE5y08dVWA==",
+ "license": "ISC"
+ },
"node_modules/@casbin/expression-eval": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@casbin/expression-eval/-/expression-eval-5.3.0.tgz",
@@ -1138,6 +1144,31 @@
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
+ "node_modules/@emnapi/core": {
+ "version": "1.11.3",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz",
+ "integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.3",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.11.3",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
+ "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/@emnapi/wasi-threads": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz",
@@ -1145,6 +1176,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
@@ -3168,7 +3200,6 @@
"resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-4.0.1.tgz",
"integrity": "sha512-68pFJgu+/AZbWkGu65Z3r55bTsCPlgyKaV4BSG8yUAD72q1PPuyVRgUwFv6BxdnibTUHlyxm06FmYWNC+bjN7A==",
"license": "MIT",
- "peer": true,
"peerDependencies": {
"@nestjs/common": "^10.0.0 || ^11.0.0",
"axios": "^1.3.1",
@@ -3323,7 +3354,6 @@
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.29.tgz",
"integrity": "sha512-zkeNRlfiQIH/044r5zphjNzKYxBRC4O00onTdCsH2qq0R30Ixo0gOS9so5TadEbJJCuGNm0Vx3PBE2FG2vkBdA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"file-type": "21.3.4",
"iterare": "1.2.1",
@@ -3370,7 +3400,6 @@
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.29.tgz",
"integrity": "sha512-ANXnZxirMNAY+JuRHCIYIdTfbhX3rizeSbJloub/4EYwpIfCuUoaz9woOofWmywYYc3gsVkLxVZV03gqntI8vA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"fast-safe-stringify": "2.1.1",
"iterare": "1.2.1",
@@ -3410,7 +3439,6 @@
"resolved": "https://registry.npmjs.org/@nestjs/graphql/-/graphql-13.4.4.tgz",
"integrity": "sha512-ZM0QLe2MTg4wLA50jtTzc9sAp5zeLqjnFryPYJdgu9ZIXEWjNPffPD8aqtDbAaSDZ3ZFLtos7tS3o3rdsILr6A==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@graphql-tools/merge": "9.2.2",
"@graphql-tools/schema": "10.0.38",
@@ -3489,7 +3517,6 @@
"resolved": "https://registry.npmjs.org/@nestjs/mongoose/-/mongoose-11.0.4.tgz",
"integrity": "sha512-LUOlUeSOfbjdIu22QwOmczv2CzJQr9LUBo2mOfbXrGCu2svpr5Hiu71zBFrb/9UC+H8BjGMKbBOq1nEbMF6ZJA==",
"license": "MIT",
- "peer": true,
"peerDependencies": {
"@nestjs/common": "^10.0.0 || ^11.0.0",
"@nestjs/core": "^10.0.0 || ^11.0.0",
@@ -3512,7 +3539,6 @@
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.29.tgz",
"integrity": "sha512-OngsAjdzM+SbbmP1XojKfBgl0Lj7u8b7IK5duwpFymzNP5EmjS8ujvqck9M8h0L14I91aRIFSjIpVfHj8WVtqw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"cors": "2.8.6",
"express": "5.2.1",
@@ -3877,7 +3903,6 @@
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
- "dev": true,
"license": "MIT",
"optional": true,
"engines": {
@@ -4141,7 +4166,6 @@
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.2.tgz",
"integrity": "sha512-sdPRb9K6iL5XZOmBubg8yiFp5yS/JdUDQsq5e6h95km91MCYMuvp7mh1fjPEYUhvHepKpZOjnEaMBR4PxjWDzg==",
"dev": true,
- "peer": true,
"dependencies": {
"@types/estree": "*",
"@types/json-schema": "*"
@@ -4278,7 +4302,6 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz",
"integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"undici-types": "~8.3.0"
}
@@ -4427,6 +4450,12 @@
"integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==",
"license": "MIT"
},
+ "node_modules/@types/validator": {
+ "version": "13.15.10",
+ "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz",
+ "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==",
+ "license": "MIT"
+ },
"node_modules/@types/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
@@ -4715,7 +4744,6 @@
"integrity": "sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.67.0",
"@typescript-eslint/types": "8.67.0",
@@ -5501,7 +5529,6 @@
"integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -5555,7 +5582,6 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"license": "MIT",
- "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -5794,6 +5820,7 @@
"resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz",
"integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"follow-redirects": "^1.16.0",
"form-data": "^4.0.6",
@@ -5806,6 +5833,7 @@
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"debug": "4"
},
@@ -5818,6 +5846,7 @@
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"agent-base": "6",
"debug": "4"
@@ -6101,7 +6130,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -6349,19 +6377,6 @@
"@types/webidl-conversions": "*"
}
},
- "node_modules/casbin-mongoose-adapter/node_modules/agent-base": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
- "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "debug": "4"
- },
- "engines": {
- "node": ">= 6.0.0"
- }
- },
"node_modules/casbin-mongoose-adapter/node_modules/bson": {
"version": "6.10.4",
"resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz",
@@ -6371,36 +6386,6 @@
"node": ">=16.20.1"
}
},
- "node_modules/casbin-mongoose-adapter/node_modules/gaxios": {
- "version": "5.1.3",
- "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz",
- "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==",
- "license": "Apache-2.0",
- "optional": true,
- "dependencies": {
- "extend": "^3.0.2",
- "https-proxy-agent": "^5.0.0",
- "is-stream": "^2.0.0",
- "node-fetch": "^2.6.9"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/casbin-mongoose-adapter/node_modules/https-proxy-agent": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
- "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "agent-base": "6",
- "debug": "4"
- },
- "engines": {
- "node": ">= 6"
- }
- },
"node_modules/casbin-mongoose-adapter/node_modules/kareem": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz",
@@ -6710,6 +6695,23 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/class-transformer": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz",
+ "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==",
+ "license": "MIT"
+ },
+ "node_modules/class-validator": {
+ "version": "0.15.1",
+ "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.15.1.tgz",
+ "integrity": "sha512-LqoS80HBBSCVhz/3KloUly0ovokxpdOLR++Al3J3+dHXWt9sTKlKd4eYtoxhxyUjoe5+UcIM+5k9MIxyBWnRTw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/validator": "^13.15.3",
+ "libphonenumber-js": "^1.11.1",
+ "validator": "^13.15.22"
+ }
+ },
"node_modules/cli-boxes": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz",
@@ -7464,7 +7466,6 @@
"integrity": "sha512-wqA7W2jbsC/BnV9Iv1UZpKVFkO1AdNoSmYW8NWG4HNOBbkAMvIqDZ27pI2f07dqn583NcIC44ckjAcOXDL1QbQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"workspaces": [
"packages/*"
],
@@ -7524,7 +7525,6 @@
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@@ -7924,7 +7924,6 @@
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.1",
@@ -8353,6 +8352,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=4.0"
},
@@ -8883,7 +8883,6 @@
"resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-7.4.0.tgz",
"integrity": "sha512-xfr+zFb/QYbs4l4ty0dltqiXIp07U6sl+tOKAb0t50/EnQek6CVVBLjETXi+FghElytvgaAWtIOt3EV7zLzIAQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@graphql-typed-document-node/core": "^3.2.0"
},
@@ -8895,7 +8894,6 @@
"version": "2.12.6",
"resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz",
"integrity": "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==",
- "peer": true,
"dependencies": {
"tslib": "^2.1.0"
},
@@ -9635,7 +9633,6 @@
"integrity": "sha512-Yi1jqNC/Oq0N4hBgNH/YvBpP1P57QqundgytzYqy3yqAa7NZPNjSoi4SGbRAXDMdBzNE6xBCi5U7RgfrvMEUVQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@jest/core": "30.4.2",
"@jest/types": "30.4.1",
@@ -10622,6 +10619,12 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/libphonenumber-js": {
+ "version": "1.13.11",
+ "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.13.11.tgz",
+ "integrity": "sha512-ETER2kMaIFTI/Nh1a8Gk03dUF/SL0VZqtI+CcVHZxp5WIHYwNS7S+uiYZDYCvLy3lOR4/DAD5jf0h5WkePPpqg==",
+ "license": "MIT"
+ },
"node_modules/limiter": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz",
@@ -11122,7 +11125,6 @@
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.9.2.tgz",
"integrity": "sha512-Nz8Rwu9jvADm0pyX0M3mEy8bx3wB5b/tRDI4XnjL7p6tZgmdyL9g0gE/EC9It6/XMIbcvniwYQDKYs91Qm+o+g==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@standard-schema/spec": "^1.1.0",
"kareem": "3.3.0",
@@ -11140,31 +11142,6 @@
"url": "https://opencollective.com/mongoose"
}
},
- "node_modules/mongoose/node_modules/gaxios": {
- "version": "7.3.0",
- "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.3.0.tgz",
- "integrity": "sha512-RB5vLV+vvQeoFPCX4QMK6/hjVkbIamPp1QSUD0CiZcnj12qbpiL+pLbYtgD+oZkWl0tl9z+o2Utp+MpM3QRhBA==",
- "license": "Apache-2.0",
- "optional": true,
- "dependencies": {
- "extend": "^3.0.2",
- "https-proxy-agent": "^7.0.1",
- "node-fetch": "^3.3.2"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/mongoose/node_modules/google-logging-utils": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.2.0.tgz",
- "integrity": "sha512-WE9av4wKDZgRjBwgVUabocx8T6/7o3Ca1Fat46FXDhXVAFibzNadedcOXrdgd1Kzmk8tsk/9ZH89Wyf/SqeZ3A==",
- "license": "Apache-2.0",
- "optional": true,
- "engines": {
- "node": ">=18"
- }
- },
"node_modules/mongoose/node_modules/mongodb": {
"version": "7.5.0",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.5.0.tgz",
@@ -11220,25 +11197,6 @@
"node": ">=20.19.0"
}
},
- "node_modules/mongoose/node_modules/node-fetch": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
- "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "data-uri-to-buffer": "^4.0.0",
- "fetch-blob": "^3.1.4",
- "formdata-polyfill": "^4.0.10"
- },
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/node-fetch"
- }
- },
"node_modules/mpath": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz",
@@ -11852,7 +11810,8 @@
"node_modules/pause": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
- "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
+ "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==",
+ "peer": true
},
"node_modules/picocolors": {
"version": "1.1.1",
@@ -11986,7 +11945,6 @@
"integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -12097,6 +12055,7 @@
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=10"
}
@@ -12238,8 +12197,7 @@
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
- "license": "Apache-2.0",
- "peer": true
+ "license": "Apache-2.0"
},
"node_modules/require-directory": {
"version": "2.1.1",
@@ -12484,7 +12442,6 @@
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"license": "Apache-2.0",
- "peer": true,
"dependencies": {
"tslib": "^2.1.0"
}
@@ -12538,7 +12495,6 @@
"integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -13484,7 +13440,6 @@
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -13689,7 +13644,6 @@
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
@@ -13889,7 +13843,6 @@
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
"dev": true,
"license": "Apache-2.0",
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -14077,6 +14030,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "peer": true,
"engines": {
"node": ">= 0.4.0"
}
@@ -14116,6 +14070,15 @@
"node": ">=10.12.0"
}
},
+ "node_modules/validator": {
+ "version": "13.15.35",
+ "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz",
+ "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -14177,7 +14140,6 @@
"integrity": "sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.8",
@@ -14425,7 +14387,6 @@
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
"integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=10.0.0"
},
diff --git a/packages/server/package.json b/packages/server/package.json
index 0fb74d50..04f4e3f0 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -25,6 +25,7 @@
"@apollo/server": "^5.5.1",
"@apollo/subgraph": "^2.14.3",
"@as-integrations/express5": "^1.1.2",
+ "@bu-sail/ra-query-core": "^0.1.2",
"@google-cloud/run": "^4.0.0",
"@google-cloud/secret-manager": "^7.0.0",
"@google-cloud/storage": "^7.22.0",
@@ -44,6 +45,8 @@
"@types/passport-jwt": "^4.0.1",
"casbin": "^5.51.1",
"casbin-mongoose-adapter": "^5.6.1",
+ "class-transformer": "^0.5.1",
+ "class-validator": "^0.15.1",
"csv-parser": "^3.2.1",
"firebase-admin": "^14.2.0",
"graphql-request": "^7.4.0",
diff --git a/packages/server/src/main.ts b/packages/server/src/main.ts
index 6835c82c..dadc31cf 100644
--- a/packages/server/src/main.ts
+++ b/packages/server/src/main.ts
@@ -1,10 +1,17 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
+import { ValidationPipe, BadRequestException } from '@nestjs/common';
+import { ValidationError } from 'class-validator';
async function bootstrap() {
const app = await NestFactory.create(AppModule, { cors: true });
+ // CORs for React Admin content-range header
+ app.enableCors({
+ exposedHeaders: ['Content-Range']
+ });
+
// Setup OpenAPI (Swagger)
const config = new DocumentBuilder()
.setTitle('SignTag API')
@@ -14,6 +21,18 @@ async function bootstrap() {
const documentFactory = () => SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, documentFactory);
+ // DTO validation
+ app.useGlobalPipes(
+ new ValidationPipe({
+ exceptionFactory: (validationErrors: ValidationError[] = []) => {
+ console.error(JSON.stringify(validationErrors));
+ return new BadRequestException(validationErrors);
+ },
+ transform: true,
+ whitelist: true
+ })
+ );
+
await app.listen(process.env.PORT || 3000);
}
bootstrap();
diff --git a/packages/server/src/project/controllers/project-admin.controller.ts b/packages/server/src/project/controllers/project-admin.controller.ts
index 7475a9c3..8d30671e 100644
--- a/packages/server/src/project/controllers/project-admin.controller.ts
+++ b/packages/server/src/project/controllers/project-admin.controller.ts
@@ -1,32 +1,74 @@
-import { Controller, Delete, Post, Put, Get } from '@nestjs/common';
-import { ApiOperation, ApiTags } from '@nestjs/swagger';
+import { Controller, Delete, Post, Put, Get, Query, Response, Body, Param, NotFoundException } from '@nestjs/common';
+import { ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger';
import { FeatureFlag } from '../../feature-flag/feature-flag.decorator';
+import { RAQuery } from '../../shared/ra-query.dto';
+import { Response as Res } from 'express';
+import { ProjectService } from '../project.service';
+import { makeContentRange } from '@bu-sail/ra-query-core';
+import { ProjectCreateV2 } from '../dtos/create.dto';
+import { Project } from '../project.model';
+import { ProjectUpdateV2 } from '../dtos/update.dto';
+import { ProjectPipe } from '../pipes/project.pipe';
-@Controller('api/admin/project')
+@Controller('api/v1/admin/projects')
@ApiTags('Project (Admin)')
export class AdminProjectController {
+ constructor(private readonly projectService: ProjectService) {}
+
@Post()
@ApiOperation({ summary: 'Create a new project' })
@FeatureFlag('SIGNTAG_ADMIN_PROJECT_ENDPOINT')
- async create() {}
+ async create(@Body() newProject: ProjectCreateV2): Promise {
+ // TODO: Check permissions
- @Get(':id')
- @ApiOperation({ summary: 'Get a single project' })
- @FeatureFlag('SIGNTAG_ADMIN_PROJECT_ENDPOINT')
- async get() {}
+ // TODO: Get organization ID from auth guard
+ return this.projectService.create(newProject, '6a832a3d17d5333f9f601db3');
+ }
@Get()
@ApiOperation({ summary: 'Get many projects' })
@FeatureFlag('SIGNTAG_ADMIN_PROJECT_ENDPOINT')
- async getMany() {}
+ async getMany(@Query() query: RAQuery, @Response() response: Res): Promise {
+ // TODO: Check permissions
+
+ // TODO: Get organization ID from auth guard
+ const projects = await this.projectService.get(query, '6a832a3d17d5333f9f601db3');
+
+ response.setHeader('Content-Range', makeContentRange('projects', projects));
+
+ return response.json(projects.data);
+ }
+
+ @Get(':id')
+ @ApiOperation({ summary: 'Get a single project' })
+ @ApiParam({ name: 'id', description: 'ID of the project to retrieve' })
+ @FeatureFlag('SIGNTAG_ADMIN_PROJECT_ENDPOINT')
+ async get(@Param('id') id: string): Promise {
+ // TODO: Check permissions
+
+ const project = await this.projectService.findById(id);
+ if (!project) {
+ throw new NotFoundException();
+ }
+ return project;
+ }
@Put(':id')
@ApiOperation({ summary: 'Update an existing project' })
+ @ApiParam({ name: 'id', description: 'ID of the project to update', type: String })
@FeatureFlag('SIGNTAG_ADMIN_PROJECT_ENDPOINT')
- async update() {}
+ async update(@Param('id', ProjectPipe) project: Project, @Body() update: ProjectUpdateV2) {
+ // TODO; Check permissions
+ return this.projectService.update(project._id, update);
+ }
@Delete(':id')
@ApiOperation({ summary: 'Delete an existing project' })
@FeatureFlag('SIGNTAG_ADMIN_PROJECT_ENDPOINT')
- async delete() {}
+ @ApiParam({ name: 'id', type: String })
+ async delete(@Param('id', ProjectPipe) project: Project): Promise {
+ // TODO: Check permissions
+ await this.projectService.delete(project);
+ return project;
+ }
}
diff --git a/packages/server/src/project/dtos/create.dto.ts b/packages/server/src/project/dtos/create.dto.ts
index 27994df6..b88dd88d 100644
--- a/packages/server/src/project/dtos/create.dto.ts
+++ b/packages/server/src/project/dtos/create.dto.ts
@@ -1,5 +1,17 @@
import { InputType, OmitType } from '@nestjs/graphql';
import { Project } from '../project.model';
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString } from 'class-validator';
@InputType()
export class ProjectCreate extends OmitType(Project, ['_id', 'created', 'organization'] as const, InputType) {}
+
+export class ProjectCreateV2 implements Omit {
+ @IsString()
+ @ApiProperty({ description: 'Human readable name to identify the project' })
+ name!: string;
+
+ @IsString()
+ @ApiProperty({ description: 'Description of the project for easier recognition' })
+ description!: string;
+}
diff --git a/packages/server/src/project/dtos/update.dto.ts b/packages/server/src/project/dtos/update.dto.ts
new file mode 100644
index 00000000..8fd437ac
--- /dev/null
+++ b/packages/server/src/project/dtos/update.dto.ts
@@ -0,0 +1,15 @@
+import { IsOptional, IsString } from 'class-validator';
+import { Project } from '../project.model';
+import { ApiProperty } from '@nestjs/swagger';
+
+export class ProjectUpdateV2 implements Partial> {
+ @IsOptional()
+ @IsString()
+ @ApiProperty({ description: 'Human readable name to identify the project', required: false })
+ name?: string;
+
+ @IsOptional()
+ @IsString()
+ @ApiProperty({ description: 'Description of the project for easier recognition', required: false })
+ description?: string;
+}
diff --git a/packages/server/src/project/project.service.ts b/packages/server/src/project/project.service.ts
index 3d75b0cf..4798c027 100644
--- a/packages/server/src/project/project.service.ts
+++ b/packages/server/src/project/project.service.ts
@@ -1,17 +1,20 @@
-import { Injectable, Inject } from '@nestjs/common';
+import { Injectable, Inject, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
-import { Project, ProjectDocument } from './project.model';
+import { Project } from './project.model';
import { ProjectCreate } from './dtos/create.dto';
import { CASBIN_PROVIDER } from '../permission/casbin.provider';
import * as casbin from 'casbin';
import { TokenPayload } from 'src/jwt/token.dto';
import { ProjectPermissions } from 'src/permission/permissions/project';
+import { PaginationResponse } from '@bu-sail/ra-query-core';
+import { RAQuery } from '../shared/ra-query.dto';
+import { ProjectUpdateV2 } from './dtos/update.dto';
@Injectable()
export class ProjectService {
constructor(
- @InjectModel(Project.name) private projectModel: Model,
+ @InjectModel(Project.name) private projectModel: Model,
@Inject(CASBIN_PROVIDER) private readonly enforcer: casbin.Enforcer
) {}
@@ -28,6 +31,19 @@ export class ProjectService {
return newProject;
}
+ async get(_query: RAQuery, organizationID: string): Promise> {
+ // TODO: Handle conversion of RAQuery into Mongoose query
+ const projects = await this.projectModel.find({ organization: organizationID });
+
+ // TODO: Include pagination results from proper query usage
+ return {
+ data: projects,
+ count: projects.length,
+ start: 0,
+ end: projects.length
+ };
+ }
+
async findById(id: string): Promise {
return this.projectModel.findById(id);
}
@@ -45,6 +61,16 @@ export class ProjectService {
await this.projectModel.findByIdAndDelete(project._id);
}
+ async update(id: string, update: ProjectUpdateV2): Promise {
+ await this.projectModel.updateOne({ _id: id }, { $set: update });
+
+ const updated = await this.findById(id);
+ if (!updated) {
+ throw new NotFoundException();
+ }
+ return updated;
+ }
+
async findAllForUser(user: TokenPayload, organization: string): Promise {
await this.enforcer.loadPolicy();
const projects = await this.findAll(organization);
diff --git a/packages/server/src/shared/ra-query.dto.ts b/packages/server/src/shared/ra-query.dto.ts
new file mode 100644
index 00000000..2f174c07
--- /dev/null
+++ b/packages/server/src/shared/ra-query.dto.ts
@@ -0,0 +1,36 @@
+import { Transform } from 'class-transformer';
+import { IsOptional } from 'class-validator';
+import { parseRange, parseSort, parseFilter, type FilterItem, type Sort, type Range } from '@bu-sail/ra-query-core';
+import { ApiProperty } from '@nestjs/swagger';
+
+export class RAQuery {
+ @IsOptional()
+ @Transform(parseSort)
+ @ApiProperty({
+ required: false,
+ type: String,
+ description: "Way to determine how to sort the results, follows FakeRest's sort specification",
+ example: '["title", "asc"]'
+ })
+ sort?: Sort;
+
+ @IsOptional()
+ @Transform(parseRange)
+ @ApiProperty({
+ required: false,
+ type: String,
+ description: "Used to restrict results to fall within a range, follows FakeRest's range specification",
+ example: '[0-9]'
+ })
+ range?: Range;
+
+ @IsOptional()
+ @Transform(parseFilter)
+ @ApiProperty({
+ required: false,
+ type: String,
+ description: "For filtering results, follows FakeRest's filter specficiation",
+ example: '{"title": "Section 1-A"}'
+ })
+ filter?: FilterItem[];
+}