create-prisma-php-app 1.11.543 → 1.11.600

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/composer.json CHANGED
@@ -17,6 +17,7 @@
17
17
  "php": "^8.1",
18
18
  "vlucas/phpdotenv": "^5.6@dev",
19
19
  "firebase/php-jwt": "dev-main",
20
- "phpmailer/phpmailer": "^6.9"
20
+ "phpmailer/phpmailer": "^6.9",
21
+ "guzzlehttp/guzzle": "7.8"
21
22
  }
22
23
  }
@@ -15,12 +15,10 @@ $dotenv->load();
15
15
 
16
16
  function determineContentToInclude()
17
17
  {
18
- $subject = $_SERVER["SCRIPT_NAME"];
19
- $dirname = dirname($subject);
20
- $requestUri = explode('?', $_SERVER['REQUEST_URI'], 2)[0];
21
- $requestUri = rtrim($requestUri, '/');
22
- $requestUri = str_replace($dirname, '', $requestUri);
23
- $uri = trim($requestUri, '/');
18
+ $scriptUrl = $_SERVER['REQUEST_URI'];
19
+ $scriptUrl = explode('?', $scriptUrl, 2)[0];
20
+ $uri = $_SERVER['SCRIPT_URL'] ?? uriExtractor($scriptUrl);
21
+ $uri = ltrim($uri, '/');
24
22
  $baseDir = APP_PATH;
25
23
  $includePath = '';
26
24
  $layoutsToInclude = [];
@@ -41,6 +39,16 @@ function determineContentToInclude()
41
39
  }
42
40
  }
43
41
 
42
+ if (empty($includePath)) {
43
+ $dynamicRoute = dynamicRoute($uri);
44
+ if ($dynamicRoute) {
45
+ $path = __DIR__ . $dynamicRoute;
46
+ if (file_exists($path)) {
47
+ $includePath = $path;
48
+ }
49
+ }
50
+ }
51
+
44
52
  $currentPath = $baseDir;
45
53
  $getGroupFolder = getGroupFolder($groupFolder);
46
54
  $modifiedUri = $uri;
@@ -69,51 +77,30 @@ function determineContentToInclude()
69
77
  return ['path' => $includePath, 'layouts' => $layoutsToInclude, 'uri' => $uri];
70
78
  }
71
79
 
72
- function checkForDuplicateRoutes()
80
+ function uriExtractor(string $scriptUrl): string
73
81
  {
74
- $routes = json_decode(file_get_contents(SETTINGS_PATH . "/files-list.json"), true);
75
-
76
- $normalizedRoutesMap = [];
77
- foreach ($routes as $route) {
78
- $routeWithoutGroups = preg_replace('/\(.*?\)/', '', $route);
79
- $routeTrimmed = ltrim($routeWithoutGroups, '.\\/');
80
- $routeTrimmed = preg_replace('#/{2,}#', '/', $routeTrimmed);
81
- $routeTrimmed = preg_replace('#\\\\{2,}#', '\\', $routeTrimmed);
82
- $routeNormalized = str_replace(['\\', '/'], DIRECTORY_SEPARATOR, $routeTrimmed);
83
- $normalizedRoutesMap[$routeNormalized][] = $route;
82
+ $prismaPHPSettings = json_decode(file_get_contents("prisma-php.json"), true);
83
+ $projectName = $prismaPHPSettings['projectName'] ?? '';
84
+ if (empty($projectName)) {
85
+ return "/";
84
86
  }
85
87
 
86
- $errorMessages = [];
87
- foreach ($normalizedRoutesMap as $normalizedRoute => $originalRoutes) {
88
- $basename = basename($normalizedRoute);
89
- if ($basename === 'layout.php') continue;
90
-
91
- if (count($originalRoutes) > 1 && strpos($normalizedRoute, DIRECTORY_SEPARATOR) !== false) {
92
- $directGroupMatchFound = false;
93
- foreach ($originalRoutes as $originalRoute) {
94
- if (preg_match('~^\\.\\/src\\/app[\\/\\\\]\\(.*?\\)[\\/\\\\].*?\\.php$~', $originalRoute, $matches)) {
95
- $directGroupMatchFound = true;
96
- }
97
- }
98
-
99
- if ($directGroupMatchFound) continue;
100
-
101
- $errorMessages[] = "Duplicate route found after normalization: " . $normalizedRoute;
102
-
103
- foreach ($originalRoutes as $originalRoute) {
104
- $errorMessages[] = "- Grouped original route: " . $originalRoute;
105
- }
88
+ $escapedIdentifier = preg_quote($projectName, '/');
89
+ $pattern = "/(?:.*$escapedIdentifier)(\/.*)$/";
90
+ if (preg_match($pattern, $scriptUrl, $matches)) {
91
+ if (!empty($matches[1])) {
92
+ $leftTrim = ltrim($matches[1], '/');
93
+ $rightTrim = rtrim($leftTrim, '/');
94
+ return "$rightTrim";
106
95
  }
107
96
  }
108
97
 
109
- if (!empty($errorMessages)) {
110
- $errorMessageString = implode("<br>", $errorMessages);
111
- modifyOutputLayoutForError($errorMessageString);
112
- }
98
+ return "/";
113
99
  }
114
100
 
115
101
  function writeRoutes()
116
102
  {
103
+ global $filesListRoutes;
117
104
  $directory = './src/app';
118
105
 
119
106
  if (is_dir($directory)) {
@@ -130,6 +117,10 @@ function writeRoutes()
130
117
  $jsonData = json_encode($filesList, JSON_PRETTY_PRINT);
131
118
  $jsonFileName = SETTINGS_PATH . '/files-list.json';
132
119
  @file_put_contents($jsonFileName, $jsonData);
120
+
121
+ if (file_exists($jsonFileName)) {
122
+ $filesListRoutes = json_decode(file_get_contents($jsonFileName), true);
123
+ }
133
124
  }
134
125
  }
135
126
 
@@ -152,6 +143,49 @@ function findGroupFolder($uri): string
152
143
  }
153
144
  }
154
145
 
146
+ function dynamicRoute($uri)
147
+ {
148
+ global $filesListRoutes;
149
+ global $dynamicRouteParams;
150
+ $uriMatch = null;
151
+ $normalizedUri = ltrim(str_replace('\\', '/', $uri), './');
152
+ $normalizedUriEdited = "src/app/$normalizedUri/route.php";
153
+ $uriSegments = explode('/', $normalizedUriEdited);
154
+ foreach ($filesListRoutes as $route) {
155
+ $normalizedRoute = trim(str_replace('\\', '/', $route), '.');
156
+ $routeSegments = explode('/', ltrim($normalizedRoute, '/'));
157
+ $singleDynamic = preg_match_all('/\[[^\]]+\]/', $normalizedRoute, $matches) === 1 && !strpos($normalizedRoute, '[...');
158
+ if ($singleDynamic) {
159
+ $segmentMatch = singleDynamicRoute($uriSegments, $routeSegments);
160
+ if (!empty($segmentMatch)) {
161
+ $trimSegmentMatch = trim($segmentMatch, '[]');
162
+ $dynamicRouteParams = [$trimSegmentMatch => $uriSegments[array_search($segmentMatch, $routeSegments)]];
163
+ $uriMatch = $normalizedRoute;
164
+ break;
165
+ }
166
+ } elseif (strpos($normalizedRoute, '[...') !== false) {
167
+ $cleanedRoute = preg_replace('/\[\.\.\..*?\].*/', '', $normalizedRoute);
168
+ if (strpos('/src/app/' . $normalizedUri, $cleanedRoute) === 0) {
169
+ if (strpos($normalizedRoute, 'route.php') !== false) {
170
+ $normalizedUriEdited = "/src/app/$normalizedUri";
171
+ $trimNormalizedUriEdited = str_replace($cleanedRoute, '', $normalizedUriEdited);
172
+ $explodedNormalizedUri = explode('/', $trimNormalizedUriEdited);
173
+ $pattern = '/\[\.\.\.(.*?)\]/';
174
+ if (preg_match($pattern, $normalizedRoute, $matches)) {
175
+ $contentWithinBrackets = $matches[1];
176
+ $dynamicRouteParams = [$contentWithinBrackets => $explodedNormalizedUri];
177
+ }
178
+
179
+ $uriMatch = $normalizedRoute;
180
+ break;
181
+ }
182
+ }
183
+ }
184
+ }
185
+
186
+ return $uriMatch;
187
+ }
188
+
155
189
  function isGroupIdentifier($segment): bool
156
190
  {
157
191
  return preg_match('/^\(.*\)$/', $segment);
@@ -159,15 +193,16 @@ function isGroupIdentifier($segment): bool
159
193
 
160
194
  function matchGroupFolder($constructedPath): ?string
161
195
  {
162
- $routes = json_decode(file_get_contents(SETTINGS_PATH . "/files-list.json"), true);
196
+ global $filesListRoutes;
163
197
  $bestMatch = null;
164
198
  $normalizedConstructedPath = ltrim(str_replace('\\', '/', $constructedPath), './');
165
199
 
166
200
  $routeFile = "/src/app/$normalizedConstructedPath/route.php";
167
201
  $indexFile = "/src/app/$normalizedConstructedPath/index.php";
168
202
 
169
- foreach ($routes as $route) {
203
+ foreach ($filesListRoutes as $route) {
170
204
  $normalizedRoute = trim(str_replace('\\', '/', $route), '.');
205
+
171
206
  $cleanedRoute = preg_replace('/\/\([^)]+\)/', '', $normalizedRoute);
172
207
  if ($cleanedRoute === $routeFile) {
173
208
  $bestMatch = $normalizedRoute;
@@ -192,10 +227,58 @@ function getGroupFolder($uri): string
192
227
  return "";
193
228
  }
194
229
 
195
- function redirect(string $url): void
230
+ function singleDynamicRoute($uriSegments, $routeSegments)
231
+ {
232
+ $segmentMatch = "";
233
+ if (count($routeSegments) != count($uriSegments)) {
234
+ return $segmentMatch;
235
+ }
236
+
237
+ foreach ($routeSegments as $index => $segment) {
238
+ if (preg_match('/^\[[^\]]+\]$/', $segment)) {
239
+ return "{$segment}";
240
+ } else {
241
+ if ($segment !== $uriSegments[$index]) {
242
+ return $segmentMatch;
243
+ }
244
+ }
245
+ }
246
+ return $segmentMatch;
247
+ }
248
+
249
+ function checkForDuplicateRoutes()
196
250
  {
197
- header("Location: $url");
198
- exit;
251
+ global $filesListRoutes;
252
+ $normalizedRoutesMap = [];
253
+ foreach ($filesListRoutes as $route) {
254
+ $routeWithoutGroups = preg_replace('/\(.*?\)/', '', $route);
255
+ $routeTrimmed = ltrim($routeWithoutGroups, '.\\/');
256
+ $routeTrimmed = preg_replace('#/{2,}#', '/', $routeTrimmed);
257
+ $routeTrimmed = preg_replace('#\\\\{2,}#', '\\', $routeTrimmed);
258
+ $routeNormalized = str_replace(['\\', '/'], DIRECTORY_SEPARATOR, $routeTrimmed);
259
+ $normalizedRoutesMap[$routeNormalized][] = $route;
260
+ }
261
+
262
+ $errorMessages = [];
263
+ foreach ($normalizedRoutesMap as $normalizedRoute => $originalRoutes) {
264
+ $basename = basename($normalizedRoute);
265
+ if ($basename === 'layout.php') continue;
266
+
267
+ if (count($originalRoutes) > 1 && strpos($normalizedRoute, DIRECTORY_SEPARATOR) !== false) {
268
+ if ($basename !== 'route.php' && $basename !== 'index.php') continue;
269
+
270
+ $errorMessages[] = "Duplicate route found after normalization: " . $normalizedRoute;
271
+
272
+ foreach ($originalRoutes as $originalRoute) {
273
+ $errorMessages[] = "- Grouped original route: " . $originalRoute;
274
+ }
275
+ }
276
+ }
277
+
278
+ if (!empty($errorMessages)) {
279
+ $errorMessageString = implode("<br>", $errorMessages);
280
+ modifyOutputLayoutForError($errorMessageString);
281
+ }
199
282
  }
200
283
 
201
284
  function setupErrorHandling(&$content)
@@ -221,19 +304,26 @@ function setupErrorHandling(&$content)
221
304
  }
222
305
 
223
306
  ob_start();
307
+ require_once SETTINGS_PATH . '/public-functions.php';
224
308
  require_once SETTINGS_PATH . '/request-methods.php';
225
309
  $metadataArray = require_once APP_PATH . '/metadata.php';
310
+ $filesListRoutes = [];
226
311
  $metadata = "";
312
+ $uri = "";
227
313
  $pathname = "";
314
+ $dynamicRouteParams = [];
228
315
  $content = "";
229
316
  $childContent = "";
230
317
 
231
318
  function containsChildContent($filePath)
232
319
  {
233
320
  $fileContent = file_get_contents($filePath);
234
- $pattern = '/<\?(?:php)?[^?]*\$childContent[^?]*\?>/is';
235
-
236
- if (preg_match($pattern, $fileContent)) {
321
+ if (
322
+ (strpos($fileContent, 'echo $childContent') === false &&
323
+ strpos($fileContent, 'echo $childContent;') === false) &&
324
+ (strpos($fileContent, '<?= $childContent ?>') === false) &&
325
+ (strpos($fileContent, '<?= $childContent; ?>') === false)
326
+ ) {
237
327
  return true;
238
328
  } else {
239
329
  return false;
@@ -243,8 +333,12 @@ function containsChildContent($filePath)
243
333
  function containsContent($filePath)
244
334
  {
245
335
  $fileContent = file_get_contents($filePath);
246
- $pattern = '/<\?(?:php\s+)?(?:=|echo|print)\s*\$content\s*;?\s*\?>/i';
247
- if (preg_match($pattern, $fileContent)) {
336
+ if (
337
+ (strpos($fileContent, 'echo $content') === false &&
338
+ strpos($fileContent, 'echo $content;') === false) &&
339
+ (strpos($fileContent, '<?= $content ?>') === false) &&
340
+ (strpos($fileContent, '<?= $content; ?>') === false)
341
+ ) {
248
342
  return true;
249
343
  } else {
250
344
  return false;
@@ -281,8 +375,10 @@ try {
281
375
  $parentLayoutPath = APP_PATH . '/layout.php';
282
376
  $isParentLayout = !empty($layoutsToInclude) && strpos($layoutsToInclude[0], 'src/app/layout.php') !== false;
283
377
 
284
- if (!containsContent($parentLayoutPath)) {
285
- $content .= "<div class='error'>The parent layout file does not contain &lt;?php echo \$content ?&gt; Or &lt;?= \$content ?&gt;<br>" . "<strong>$parentLayoutPath</strong></div>";
378
+ $isContentIncluded = false;
379
+ $isChildContentIncluded = false;
380
+ if (containsContent($parentLayoutPath)) {
381
+ $isContentIncluded = true;
286
382
  }
287
383
 
288
384
  ob_start();
@@ -293,13 +389,16 @@ try {
293
389
  $childContent = ob_get_clean();
294
390
  }
295
391
  foreach (array_reverse($layoutsToInclude) as $layoutPath) {
296
- ob_start();
297
- if ($parentLayoutPath === $layoutPath) continue;
392
+ if ($parentLayoutPath === $layoutPath) {
393
+ continue;
394
+ }
395
+
298
396
  if (containsChildContent($layoutPath)) {
299
- require_once $layoutPath;
300
- } else {
301
- $content .= "<div class='error'>The layout file does not contain &lt;?php echo \$childContent ?&gt; Or &lt;?= \$childContent ?&gt<br>" . "<strong>$layoutPath</strong></div>";
397
+ $isChildContentIncluded = true;
302
398
  }
399
+
400
+ ob_start();
401
+ require_once $layoutPath;
303
402
  $childContent = ob_get_clean();
304
403
  }
305
404
  } else {
@@ -314,11 +413,19 @@ try {
314
413
  $childContent = ob_get_clean();
315
414
  }
316
415
 
317
- $content .= $childContent;
318
-
319
- ob_start();
320
- require_once APP_PATH . '/layout.php';
321
- echo ob_get_clean();
416
+ if (!$isContentIncluded && !$isChildContentIncluded) {
417
+ $content .= $childContent;
418
+ ob_start();
419
+ require_once APP_PATH . '/layout.php';
420
+ } else {
421
+ if ($isContentIncluded) {
422
+ $content .= "<div class='error'>The parent layout file does not contain &lt;?php echo \$content; ?&gt; Or &lt;?= \$content ?&gt;<br>" . "<strong>$parentLayoutPath</strong></div>";
423
+ modifyOutputLayoutForError($content);
424
+ } else {
425
+ $content .= "<div class='error'>The layout file does not contain &lt;?php echo \$childContent; ?&gt; or &lt;?= \$childContent ?&gt;<br><strong>$layoutPath</strong></div>";
426
+ modifyOutputLayoutForError($content);
427
+ }
428
+ }
322
429
  } catch (Throwable $e) {
323
430
  $content = ob_get_clean();
324
431
  $content .= "<div class='error'>Unhandled Exception: " . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8') . "</div>";
package/dist/index.js CHANGED
@@ -189,11 +189,7 @@ const ws = new WebSocket("ws://localhost:8080");
189
189
  async function createUpdateGitignoreFile(baseDir, additions) {
190
190
  const gitignorePath = path.join(baseDir, ".gitignore");
191
191
  if (checkExcludeFiles(gitignorePath)) return;
192
- // Check if the .gitignore file exists, create if it doesn't
193
192
  let gitignoreContent = "";
194
- if (fs.existsSync(gitignorePath)) {
195
- gitignoreContent = fs.readFileSync(gitignorePath, "utf8");
196
- }
197
193
  additions.forEach((addition) => {
198
194
  if (!gitignoreContent.includes(addition)) {
199
195
  gitignoreContent += `\n${addition}`;
@@ -205,6 +201,7 @@ async function createUpdateGitignoreFile(baseDir, additions) {
205
201
  }
206
202
  // Recursive copy function
207
203
  function copyRecursiveSync(src, dest, answer) {
204
+ var _a;
208
205
  console.log("🚀 ~ copyRecursiveSync ~ dest:", dest);
209
206
  console.log("🚀 ~ copyRecursiveSync ~ src:", src);
210
207
  const exists = fs.existsSync(src);
@@ -212,14 +209,18 @@ function copyRecursiveSync(src, dest, answer) {
212
209
  const isDirectory = exists && stats && stats.isDirectory();
213
210
  if (isDirectory) {
214
211
  const destLower = dest.toLowerCase();
215
- console.log("🚀 ~ copyRecursiveSync ~ destLower:", destLower);
216
- const destIncludeWebsocket = destLower.includes("src\\lib\\websocket");
217
- console.log(
218
- "🚀 ~ copyRecursiveSync ~ destIncludeWebsocket:",
219
- destIncludeWebsocket
220
- );
221
212
  if (!answer.websocket && destLower.includes("src\\lib\\websocket")) return;
222
213
  if (!answer.prisma && destLower.includes("src\\lib\\prisma")) return;
214
+ const destModified = dest.replace(/\\/g, "/");
215
+ if (
216
+ (_a =
217
+ updateAnswer === null || updateAnswer === void 0
218
+ ? void 0
219
+ : updateAnswer.excludeFilePath) === null || _a === void 0
220
+ ? void 0
221
+ : _a.includes(destModified)
222
+ )
223
+ return;
223
224
  if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
224
225
  fs.readdirSync(src).forEach((childItemName) => {
225
226
  copyRecursiveSync(
@@ -313,16 +314,8 @@ function modifyLayoutPHP(baseDir, useTailwind) {
313
314
  async function createOrUpdateEnvFile(baseDir, content) {
314
315
  const envPath = path.join(baseDir, ".env");
315
316
  if (checkExcludeFiles(envPath)) return;
316
- let envContent = fs.existsSync(envPath)
317
- ? fs.readFileSync(envPath, "utf8")
318
- : "";
319
- // Check if the content already exists in the .env file
320
- if (!envContent.includes(content)) {
321
- envContent += `${envContent !== "" ? "\n\n" : ""}${content}`;
322
- fs.writeFileSync(envPath, envContent, { flag: "w" });
323
- } else {
324
- console.log(".env file already contains the content.");
325
- }
317
+ console.log("🚀 ~ content:", content);
318
+ fs.writeFileSync(envPath, content, { flag: "w" });
326
319
  }
327
320
  function checkExcludeFiles(destPath) {
328
321
  var _a, _b;
@@ -355,10 +348,7 @@ async function createDirectoryStructure(baseDir, answer) {
355
348
  ? void 0
356
349
  : updateAnswer.isUpdate
357
350
  ) {
358
- filesToCopy.push(
359
- { src: "/.env", dest: "/.env" },
360
- { src: "/tsconfig.json", dest: "/tsconfig.json" }
361
- );
351
+ filesToCopy.push({ src: "/tsconfig.json", dest: "/tsconfig.json" });
362
352
  if (updateAnswer.tailwindcss) {
363
353
  filesToCopy.push(
364
354
  { src: "/postcss.config.js", dest: "/postcss.config.js" },
@@ -412,7 +402,19 @@ AUTH_SECRET=uxsjXVPHN038DEYls2Kw0QUgBcXKUyrjv416nIFWPY4=
412
402
  # SMTP_ENCRYPTION=ssl or tls
413
403
  # MAIL_FROM=john.doe@gmail.com
414
404
  # MAIL_FROM_NAME="John Doe"`;
415
- await createOrUpdateEnvFile(baseDir, prismaPHPEnvContent);
405
+ if (answer.prisma) {
406
+ const prismaEnvContent = `# Environment variables declared in this file are automatically made available to Prisma.
407
+ # See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
408
+
409
+ # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
410
+ # See the documentation for all the connection string options: https://pris.ly/d/connection-strings
411
+
412
+ DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"`;
413
+ const envContent = `${prismaEnvContent}\n\n${prismaPHPEnvContent}`;
414
+ await createOrUpdateEnvFile(baseDir, envContent);
415
+ } else {
416
+ await createOrUpdateEnvFile(baseDir, prismaPHPEnvContent);
417
+ }
416
418
  // Add vendor to .gitignore
417
419
  await createUpdateGitignoreFile(baseDir, ["vendor", ".env", "node_modules"]);
418
420
  }
@@ -629,7 +631,10 @@ async function main() {
629
631
  console.log(chalk.red("Installation cancelled."));
630
632
  return;
631
633
  }
632
- execSync(`npm install -g create-prisma-php-app`, { stdio: "inherit" }); // TODO: Uncomment this line before publishing the package
634
+ execSync(`npm uninstall -g create-prisma-php-app`, { stdio: "inherit" });
635
+ execSync(`npm install -g create-prisma-php-app@test-update`, {
636
+ stdio: "inherit",
637
+ }); // TODO: Uncomment this line before publishing the package
633
638
  // Support for browser-sync
634
639
  execSync(`npm install -g browser-sync`, { stdio: "inherit" });
635
640
  // Create the project directory
@@ -744,12 +749,25 @@ async function main() {
744
749
  JSON.stringify(prismaPhpConfig, null, 2),
745
750
  { flag: "w" }
746
751
  );
747
- execSync(
748
- `C:\\xampp\\php\\php.exe C:\\ProgramData\\ComposerSetup\\bin\\composer.phar install`,
749
- {
750
- stdio: "inherit",
751
- }
752
- );
752
+ if (
753
+ updateAnswer === null || updateAnswer === void 0
754
+ ? void 0
755
+ : updateAnswer.isUpdate
756
+ ) {
757
+ execSync(
758
+ `C:\\xampp\\php\\php.exe C:\\ProgramData\\ComposerSetup\\bin\\composer.phar update`,
759
+ {
760
+ stdio: "inherit",
761
+ }
762
+ );
763
+ } else {
764
+ execSync(
765
+ `C:\\xampp\\php\\php.exe C:\\ProgramData\\ComposerSetup\\bin\\composer.phar install`,
766
+ {
767
+ stdio: "inherit",
768
+ }
769
+ );
770
+ }
753
771
  console.log(
754
772
  `${chalk.green("Success!")} Prisma PHP project successfully created in ${
755
773
  answer.projectName
@@ -17,6 +17,8 @@ model User {
17
17
  password String?
18
18
  emailVerified DateTime?
19
19
  image String?
20
+ createdAt DateTime @default(now())
21
+ updatedAt DateTime @updatedAt
20
22
 
21
23
  roleId Int?
22
24
  userRole UserRole? @relation(fields: [roleId], references: [id])