Tom Gobich

@tomgobich

Developer & dog lover. I teach AdonisJS, a full-featured Node.js framework, at Adocasts where I publish weekly lessons. Professionally, I work with JavaScript, .NET, and SQL Server.

Adocasts

Burlington, KY

Learning initiated
156
Lessons started
Knowledge gained
72
Lessons completed
Total learning time
9h 46m
Spent learning
Community impact
470
Comments & forum posts

Recent Activity

Here's what @tomgobich has been up to this past year

  • Replied to Hi Tom,I'm having issues with messages between layouts (I guess...

    Hi german-gonzalez!

    Do you also have the runToasts call within the onMounted hook? Since the ToastsManager is added via the layout, when you go from a page using the AppLayout to a page using the AuthLayout the AppLayout's ToastManager will be destroyed and a new one will be created and mounted for the AuthLayout. By having runToasts called inside the onMounted hook, we're populating that initial state for our messages when the component is first created and ready.

    With how we have our layouts defined when we traverse between pages using the same layout, like going from the login page to the register page, the same layout instance will be used. Since our ToastsManager is added via the layout, that also means the same ToastManager instance will be used and that is where the watchEffect comes into play. That will watch the reactive values we're using within the hook for changes and rerun when any are detected.

    onMounted(() => {
      runToasts({
        exceptions: props.messages.errorsBag,
        success: props.messages.success,
      })
    })
    
    watchEffect(() => {
      runToasts({
        exceptions: props.messages.errorsBag,
        success: props.messages.success,
      })
    })
    Copied!

    Why does it work when wrapped in $nextTick? The issue is that watchEffect runs immediately to pickup which reactive values to watch for changes. When it runs immediately, its running in the setup stage of our component at which time the component hasn't been mounted, so we don't have a template yet. That results in nothing happening with our toasts. By wrapping it in $nextTick you're deferring when the watchEffect's runToasts call actually run.

    Hope this helps!!

  • Replied to Thank you very much for your detailed explanation, I have a ...

    Awesome, I'm glad to hear that helped! Anytime!!

  • Upvoted comment _method is not documented btw

  • Replied to _method is not documented btw

    Hi iphonegeek-me!

    Method spoofing with _method is documented on the request page. I also introduce it in this series in our HTTP Method Spoofing HTML Forms lesson.

  • Replied to Hey @tomgobich I was going through Adocast’s source code for...

    Hey Tresor!

    Password Reset

    The differences between using signed URLs and a database-stored token for password resets mostly come down to weighing the pros and cons, and how important those are for your site's use case.

    Signed URLs are reliable and quick to implement. However, since they aren't stored in the database, we need a way to attach them to a user, like adding the user's email or ID to the signed URL. Additionally, once generated, we have no way to invalidate the token until its expiry time is reached. This means we can't expire a token after it's been used. This will suffice for most basic apps and is a fine option if you're just starting out with a small user base.

    Database-stored tokens have a bit more overhead to implement, and we need to manage the tokens ourselves. However, since the token is stored in the database we don't need to expose any of our users' information via the URL. Additionally, we have the ability to update the database record at any time, so it can easily be expired once used. This is a great option if protecting your users info, even from their own inbox, is a top priority. It's also a must-use if you need to invalidate a token after use (for example, say you need to use an expiry longer than you'd feel comfortable leaving lingering).

    The third option here is to use both, generate a token then sign that token into the URL. This would be the most secure option. It takes away the cons of just using a Signed URL and adds an extra obfuscation layer to the database-stored token. Might be seen as a bit overkill depending on the app though.

    So, none of the above are bad options unless the type of site you're building or its audience dictates a higher level of security. I actually plan on updating Adocasts password reset flow to use database-stored tokens here in the future. I almost did it with a redesign I'm working on, but opted to save it till after.

    Deferred Mail Sending

    If we actually dig into sendLater and poppinss/defer, both are in-memory queues using fastq under the hood to actually do the queuing. So, I'd argue there's no real difference between the two unless one has a specific feature you're looking for.

    The downside to an in-memory queue is that if your server crashes or reboots (like when you deploy) then anything in that queue has been lost. If you don't send many emails and don't deploy all that often then the likelihood of a lost email is pretty slim.

    If you send emails frequently or deploy frequently then your chances of a lost email increase. You might also be sending mission-critical emails that must reach their destination (think appointment confirmation or order receipt emails). That's where an alternative like BullMQ can help because it'll hold the queued item until it's processed, even if the app reboots. You can also swap sendLater's in-memory queue with BullMQ as well. So, you can start with in-memory and upgrade the queue system very easily down the road without having to rewrite your sendLater code.

    I'm still using an in-memory queue for Adocasts emails, but I don't deploy all that often and Stripe takes care of our purchase-based emails.

    Hope this helps!!

  • Replied to Thanks Tom, Sadegh opened a pr for that and it is about to close...

    Sure thing! Yeah, looks promising! 🤞

  • Published lesson Rate Limiting an Organization's HTTP Requests

  • Replied to In case you already using ace migration:refresh- —seed like ...

    Terribly sorry you lost some time on that christoxz!! I've written down a note to circle back to lesson 4.8, where we introduce the fake_seeder, and add a note about the seeder order of execution.

  • Published lesson User-Defined Relationship Loading

  • Published lesson Filtering Lessons by Publication Date

  • Replied to More info here: [Empty fields lead to empty strings](https:/...

    Terribly sorry about that adun! I'm happy to see you were able to figure it out, and your assessment is spot-on! I'll circle back and insert a heads-up into this lesson. Thank you very much for sharing, and sorry again this got past me!

  • Published lesson Making our Search Course Action Easily Reusable

  • Published lesson Searching and Filtering Lessons

  • Replied to Ah thanks for the response, I think I may have just skimmed ...

    Yeah, I completely agree!! Thanks again for the helpful feedback!!

  • Replied to I believe this redis module needs an active Redis server deployed...

    Hi holodevelop! First, thank you for your feedback! I'm always looking to improve my lesson making skills, and feedback like this definitely helps, I appreciate it!

    To answer your question, any Redis connection would do. You can even use a small free tiered one from Redis Cloud. However, we only use Redis in this lesson and the next within this series, so just skipping over is also a valid option.

    There's many things about this series I would add, remove, or do differently now if I were to go back and do them again. A heads up blurb like the below in this lesson and our before we begin lesson would definitely be one of those things.

    As a heads up, we'll only be utilizing Redis in this lesson and the next as a way to demonstrate installing, configuring, and using the package. Again, as we covered in lesson 1.1, you'll need a Redis connection to work with. If you're following along and don't see yourself using Redis for anything beyond this series, feel free to skip this and the next lesson and pick back up in lesson 2.16.

    👆 I'll circle back and add that to this lesson and another blurb to lesson 1.1, I have some updates to do in module 11 as well.

  • Published lesson Filtering by a Number or Array of Numbers

  • Replied to Great lessons, but I had real trouble on this one. I'm new to...

    Terribly sorry about that drummersi-3! This module wasn't originally a part of this series, I added it on last minute and that really shows in this lesson! I'll have to re-watch this module and think through how to appropriately correct it. Again - apologies you had difficulty here, there's definitely room for improvement on this lesson.

  • Published lesson Advanced String Filtering

  • Published lesson Basic Course Search & Filter

  • Replied to Hey Tom, do you think shadcn/vue new version will be available...

    Hey hsn! Yeah, I'm not sure what the timetable looks like on that. If you're following along with this series, I'd recommend sticking with Radix for now. If that issue gets closed with a happy migration possible from Radix to Reka for AdonisJS apps, which it sounds like it will (just a matter of when), I'll take a look at adding a 14th module to this series to cover that migration.

  • Published lesson Getting A Module's Lessons

  • Replied to Oh, thanks, I didn’t know about the SSR allowlist. My main concern...

    Yeah, that can definitely be a major headache depending on the UI library! Anytime!! 😊

  • Published lesson Fixing Our ESLint Integration

  • Published lesson Lesson Operations

  • Replied to @tomgobich Thank you for this great series. Are there any...

    Hi Tresor! Yeah, there's some benefits to that, but also cons. The benefits are that you can easily ensure those pages where SEO is important have as small of a weight to them as possible. Many times marketing materials have a different look and feel from the internal application as well, so having them completely split between your Edge pages & Inertia app can help define that barrier.

    Though Google and other search engines have been getting better about reading JavaScript, you'll also have piece of mind in knowing exactly what markup is being sent when using Edge as well.

    The main con is for those instances where there isn't a visual separation between marketing materials and the internal application. For example, say we're using a UI library in the SPA app but we still want that same look on the Edge pages. We'd need to replicate that UI library's CSS for our Edge pages in order for the look and feel to remain the same. Of course, that may not be an issue if your UI library is framework agnostic or uses web components, but if you're using something like ShadCN, Nuxt UI, etc then that can be a real issue.

    In those cases, or even if you just prefer to have everything reside within Inertia, you can specify which pages in Inertia should use SSR. The SSR allowlist resides within the config/inertia.ts configuration file and accepts an array of page names or a function that returns a boolean.

    Below are two examples from the linked documentation for this.

    import { defineConfig } from '@adonisjs/inertia'
    
    export default defineConfig({
      ssr: {
        enabled: true,
        pages: ['home']
      }
    })
    Copied!
    import { defineConfig } from '@adonisjs/inertia'
    
    export default defineConfig({
      ssr: {
        enabled: true,
        pages: (ctx, page) => !page.startsWith('admin')
      }
    })
    Copied!
  • Published lesson Getting Just Course Modules & Lessons

  • Replied to Hey thanks for the tips. One question though, what would be ...

    Hi Diego! That's a great question! My approach would vary depending on just how segmented I would need to get at the data. Let's take a Post model as an example. A post might have certain types (blog, article, news, etc). If the type is the primary way I need to fetch the post, I might have a getPostsByTypeId(typeId: number) service method or action.

    However, typically there are other checks needed for a post as well

    • Fetching only those that are published, not published, or all

    • Fetching posts tied to a specific taxonomy/topic

    • Ordering by publication date, title, or something else

    • Should we paginate the results or get only a specific number back

    And, the list could go on from there. Our endpoints will vary from caring about none through all of these.

    My approach to this is to stop and think about what most endpoints are going to need now or shortly in the future. I don't worry about issues we might have 3 or 5 years down the road, as those may or may not come to fruition and we can always refactor to appease them if they do.

    I'd then create methods (service or action) to satisfy the requirements of most of the endpoints I'll need. This might result in:

    • getPosts(stateId: number, typeId: number, limit?: number = 10, orderBy: PostSorts = 'latest')
      This would return the first limit number of published posts for the provided type and state ordered by sorts we have defined for our posts (like latest, alphabetical, popular, etc). Type would be whether it's a blog, article, etc and state would determine if it's published or not.

    • getPostsPaginated(typeId: number, pagination: PaginatorQuery, orderBy: PostSorts = 'latest')
      This would be similar to getPosts, but would return a paginator instead of the first limit. PaginatorQuery here would contain the page, perPage, and baseUrl

    That alone would satisfy most of the way we would need to get at our posts, however, a really nice part about Lucid's query builder is that the query can be built on top of further until it is either awaited, executed, or we call a method that does not return back the builder, like paginate.

    Meaning, we could return the Post Query Builder from these methods giving us the ability to expand off of them as needed, which is really convenient for those outliers. We might also choose to call these queryPosts instead of get to note that they're expandable. For example:

    export default class QueryPosts {
      static handle(
        stateId: number, 
        typeId: number, 
        limit?: number = 10, 
        orderBy: PostSorts = 'latest'
      ): ModelQueryBuilderContact<typeof Post, Post> {
        const query = await Post.query()
          .where({ stateId })
          .where({ typeId })
          .limit(limit)
    
        switch (orderBy) {
          case PostSorts.ALPHABETICAL:
            query.orderBy('title', 'asc')
            break
          default:
            query.orderBy('publishAt', 'desc')
            break
        }
    
        return query
      }
    }
    
    const query = QueryPosts.handle(States.PUBLISHED, typeId: PostTypes.BLOG)
    const blogs = await query
      .whereHas('topics', (topic) => topic.where('topics.slug', slug))
    Copied!

    Now, with this in mind and also that paginate won't allow this, we could remove the limit parameter from this method, get rid of our getPostsPaginated method and instead perform those outside of these methods for additional expansiveness.

    If desired, we can create additional methods that call and build off our getPosts as well. Like, the above blog query could be wrapped in a getPostsByTopic(slug: string) method. This comes in handy if this is a frequent way we're going to get at our posts. If we're only ever going to need this for a single page, we might chose to leave the query as-is from the above previous example.

    I also really like giving the more complex statements in my queries names. This can be done via query scopes or you can go a step further and create a builder. A builder is essentially a query builder wrapping the Model Query Builder itself with chainable methods specific to your application. That might look like the below.

    export default class PostBuilder extends BaseBuilder<typeof Post, Post> {
      whereHasTopic(slug: string) {
        this.query.whereHas((query) => query.where('topics.slug', slug)
        return this
      }
    
      // use ids
      whereTypeId(typeId: PostTypes) {
        this.query.where({ typeId })
        return this
      }
    
      // or use named methods
      wherePublished() {
        this.query.where('stateId', States.PUBLISHED)
        return this
      }
    
      whereNotPublished() {
        this.query.whereNot('stateId', States.PUBLISHED)
        return this
      }
    
      // or offer both
      whereStateId(stateId: States) {
        this.query.where({ stateId })
        return this
      }
    
      orderLatest() {
        this.query.orderBy('publishAt', 'desc')
        return this
      }
    }
    
    const blogs = await new PostBuilder()
      .whereHasTopic('adonisjs')
      .wherePublished()
      .whereTypeId(PostTypes.BLOG)
      .orderLatest()
    Copied!

    What I like about this is that it keeps our queries very easy to build and understand. It also keeps any methods we may choose to wrap this with small and easily reusable as well. Which, in turn, makes it less of a chore to have multiple different methods for getting at our posts. This is actually the approach I've used for the Adocasts site, if you'd like to see a full example.

    Sorry this was a long answer, TLDR: the best approach will vary depending on how many varying ways you need to query your data. If it's only a few ways, specific methods will do just fine. If you need it a myriad of different ways, more complex or even a combination of solutions may prove fruitful.

  • Published lesson Getting A Course's Details, Modules, & Lessons

  • Published lesson Updating A Course's Difficulty, Status, or Access Level

  • Published lesson Course Module Nested Resource

  • Replied to First to comment Glad to be the first to comment, glad to get...

    Thanks a ton for watching, Odejayi! I'm happy to hear you're enjoying AdonisJS! 😊

  • Published lesson Listing Courses

  • Published lesson Creating, Updating, and Deleting Courses

  • Published lesson Paginating our Course List

  • Replied to How can I make use of route identifiers ? Especially for programmatic...

    Hi Jean!

    You can achieve that by using Tuyua! From it's documentation, once installed & configured it'll allow template & programmatic linking using route identifiers like the below:

    <script setup lang="ts">
    import { Link, useRouter } from '@tuyau/inertia/vue'
    
    const router = useRouter()
    
    function visit() {
      router.visit('users.posts.show', { id: 1, postId: 2 })
    }
    </script>
    
    <template>
      <div>
        <Link route="users.posts.show" :params="{ id: 1, postId: 2 }">Go to post</Link>
        <button type="button" @click="visit">Go to posts</button>
      </div>
    </template>
    Copied!
    • inertia
    • pages
    • Home.vue
  • Replied to If we do something like this in react ``` resolve: async (name...

    Hi cubicalprayer712!

    Vladimir kindly left steps on getting this setup in React in the next lesson's comments if you'd like more details, but something like the below should satisfy the needed typing.

    const page = await resolvePageComponent(
      `../pages/${name}.tsx`,
      import.meta.glob('../pages/**/*.tsx'),
    ) as {
      default: ComponentType & { layout?: (page: ReactElement) => ReactElement }
    }
    
    page.default.layout = page.default.layout
    Copied!
  • Replied to Thank you for the answer, that makes a lot of sense. Also you...

    Anytime, Vladimir!

    That's good to know that it does work! If it works now, I'm more than sure it'll continue to work down the road 😊

  • Replied to Thanks Tom, I was working on a project that was initialized ...

    Hi cubicalprayer712! Yes, Inertia's structure is a little different from the web starter kit including the name/location of it's base EdgeJS page! Happy to hear you were able to find it!

  • Replied to Hi Tom, Thanks to your Adonis Basics course, I was able to ...

    Hi Vladimir!

    That's awesome to hear, I'm happy you were able to build and get an app out to production!

    As for your question, the main disadvantage comes if you're working with a team as it'll increase the possibility of merge conflicts or conflicting sorts for your migrations when merging.

    Another disadvantage, though much smaller, is that you might need to constantly tweak ordering as you're working on things. For example, if you're working on commit that is adding 5 migrations (10, 11, 12, 13, 14) and you realize you need to add a new migration but it needs to run before the other 5, you'll have to reorder those existing 5. Again, that's a small issue, but using timestamps would've likely given you plenty of buffer where that wouldn't be the case.

    The timestamp prefix can also be useful if you need to know when a migration was made; though you can also use the adonis_schema table to see when the migration was run which will be similar.

    Apart from that, I've never tried this but I believe Lucid should handle it just fine and can't think of any other big gotchas.

  • Replied to Hello Tom, is it possible to display the same toast multiple...

    Hi memsbdm! With how we have it, it should display the toast for every failed login attempt. Vue Sonner stacks the messages by default, but you can see each individually if you hover over the stack.

    The key premise of it is that we're watching the effect of our messages prop. When the prop's proxy detects a mutation, our watchEffect is called, which pushes a new toast into Vue Sonner.

    Try adding a console log within the watchEffect so you can see exactly when it is firing and the message(s) the mutation contains.

    
    watchEffect(() => {
      console.log('ToastManager: messages changed', { ...props.messages })
      runToasts({
        exceptions: props.messages.errorsBag,
        success: props.messages.success,
      })
    })
    Copied!
    • inertia
    • components
    • ToastManager.vue

    It should look something like the below:

  • Published lesson Access Level API CRUD

  • Published lesson Status API CRUD

  • Published lesson API Authorization Checks

  • Replied to Yeah, now I see, if it takes more time to extract it's ok to...

    Exactly! It's all a balancing act. Anytime, inox!! 😊

  • Upvoted comment Thanks for this course.

  • Replied to Thanks for this course.

    Thank you for watching and for your feedback, jals! I hope you enjoyed!

  • Replied to Hey, since vue-sonner updated to V2 we just need to add import...

    Thank you very much for sharing, memsbdm!! 😊

  • Replied to (PART 3) What the code above basically tells Inertia is this...

    Thanks so much for sharing, Vladimir!!

  • Upvoted comment Ok, we can use mixins

  • Replied to Ok, we can use mixins

    Sorry for the delayed response!! Yes, if you'd like, you can definitely use mixins to extract those out of your models into a reusable bit of code. That would look very similar to what we do in the next lesson with our organization!

    Personally, I never bother to do this with my timestamps because when we use the Ace CLI to create our migrations & models those timestamps come with that stub automatically, so it is actually more work to extract them than it is to just leave them be. Plus, they're consistent enough to be easily updated across the board with a find/replace if needed.

  • Published lesson Listing Organization Difficulties

  • Published lesson Creating Organization Difficulties

  • Published lesson Getting A Specific Difficulty

  • Published lesson Updating A Difficulty

  • Published lesson Deleting A Difficulty

  • Replied to I'm creating a similar project. I have a controller for users...

    Hi Martin!

    Sounds like it is most likely a reactivity issue. When the redirect occurs, Inertia will use the new data to update the props of the page. If the reactivity from the props is severed, then that would cause those props and your view to become out of sync.

    You should be able to use Vue Devtools to inspect and verify that the props are actually getting updated. Then, follow your usage of that prop to see exactly where the reactivity is being severed.

    You could also do a simplified sanity check to ensure that the flow is indeed working.

    <script setup>
    defineProps({
      users: UserDto[]
    })
    </script>
    
    <template>
      <ul>
        <li v-for="user in users" :key="user.id">
          {{ user.email }}
        </li>
      </ul>
    </template>
    Copied!
    • inertia
    • pages
    • Usuarios.vue
  • Upvoted comment That fixed it, thanks!

  • Replied to That fixed it, thanks!

    Great to hear, and thanks for confirming Aaron! I'll get that note added into lesson 12.3! Again, terribly sorry about that!

  • Replied to Just my finding about @adonisjs/inertia update is that if you...

    Hi inox! Yes, that's correct! You have to send the request via the useForm composable in order for that composable to be able to digest any validation errors into its state.

  • Replied to I am getting an error from the SettingsShell.vue, I am guessing...

    Hi Aaron! I had run into this as well while preparing for the Web API series, but my thinking at the time was that it was something that had changed during the package updates when updating to Inertia 2. Perhaps not though, if you're seeing it here. Terribly sorry about that! I'll have to circle back and add a note in lesson 12.3.

    If you go into your inertia/css/app.css file and add the below, does that resolve this issue for you? My understanding was that TailwindCSS should be able to discern the default config when omitted, but it appears @apply might not work so well with that.

    @config "../../tailwind.config.js";
    Copied!
  • Replied to Hi Tom,Thanks again for the support. I spent the whole day fixing...

    Awesome, thank so much Abdelmadjid! I was able to get it installed and working, and after an initial scan it looks great! Well done on this!! 😃

  • Replied to discussion Averaging over time period and grouping by properties

    Hi swarnim! What queries have you tried? I'm not sure I'm fully following.

    If your timestamp is stored as a string within the database rather than a date-like type then I'd only see one option. Query all results within the range, convert your timestamp into a date post-query and then group by the needed date frequency. I guess, alternatively, you could also try to group by the left portion of the string consisting of the date.

    If it is a date-like type then you should be able to use groupByRaw to group & sum by the needed date frequency. How that looks will vary depending on your database (MySQL, PostgreSQL, etc). https://knexjs.org/guide/query-builder.html#groupbyraw

  • Upvoted discussion about websockets

  • Replied to discussion about websockets

    Hi Manas! Unfortunately, we don't have any content on WebSockets at this time. AdonisJS does have a first-party package for Server Sent Events (SSE) called Transmit. This sounds like it could be suitable for your use-case and may be easier to implement!

  • Replied to discussion EdgeJS extension for Zed code editor

    This looks great, Abdelmadjid!

    Is there any trick to getting it installed within Zed? I tried dropping the repo into Zed's extensions directory but it doesn't show within Zed itself.

    ~/Library/Application Support/Zed/extensions/installed/zed-edge
  • Upvoted discussion EdgeJS extension for Zed code editor

  • Published lesson The Goal of our REST API

  • Published lesson Our First API Endpoint to Get Our Organization's Details

  • Published lesson Setting Up Our REST Client

  • Published lesson Simple API Versioning

  • Upvoted comment Thank you for your response.

  • Replied to Thank you for your response.

    Anytime!!

  • Replied to Hi, if a request is NOT a HTMX I want the response to be embedded...

    Hi virimchi-m!

    Yep, you could absolutely do that! If the HX-Request header is there then HTMX sent the request, so you can use that to conditionally determine whether to render your layout or not. If needed you can also use the target, via the HX-Target header, to determine if the layout needs rerendered.

    Alternatively, you could put the contents you want to render within a component, very similar to how we did it in this series with our post_list then render that component directly!

    <ul>
      @each (post in posts)
        <li>{{ post.title }}</li>
      @endeach
    </ul>
    Copied!
    • resources
    • views
    • components
    • post
    • list.edge

    Then, we can use this component within the page:

    @layout()
      @!post.filter()
      @!post.list({ posts }) {{-- 👈 renders the list for the page --}}
    @end
    Copied!
    • resources
    • views
    • pages
    • posts
    • index.edge

    We can conditionally render it directly from our controller.

    export default class PostController {
      async index({ view, request }: HttpContext) {
        const isHtmx = !!request.header('HX-Request')
        const posts = [/* ... */]
       
        return isHtmx
          ? view.render('components/post/list', { posts })
          : view.render('pages/posts/index', { posts })
      }
    }
    Copied!

  • Replied to Everything works except the delete. I get delete from organizations...

    Hey Aaron!

    What you're getting is a foreign key constraint error. You get this when you attempt to delete a record whose id is referenced by another record's foreign key. Foreign keys are essentially in charge of id integrity within our database, ensuring that we aren't referencing any ids that don't actually exist.

    For example, if I have an organization with an id of 3 and a course with an id of 1 that contains an organization_id of 3. When I attempt to delete the organization with an id of 3 the course record, via it's foreign key constraint, will prevent the organization from being deleted because the course's organization_id wouldn't exist anymore. As such, the course with an organization_id of 3 must be deleted before we can actually delete the organization with an id of 3.

    Throughout this series, we don't need to worry to much about any of this because when we setup our database, through our migrations, we added instructions to our database to cascade deletions. Meaning, if we attempt to delete the organization with an id of 3, our database will automatically cascade that deletion to also delete our courses with an organization_id of 3.

    table
      .integer('organization_id')
      .unsigned()
      .references('organizations.id')
      .onDelete('CASCADE') // 👈
      .notNullable()
    Copied!

    It sounds like you might be missing one or more of these cascade instructions. The error you're getting should include the foreign key's name that prevented the deletion. You should be able to use the foreign key's name to determine which relationship is not cascading. I believe you should then be able to switch it to cascade using something like the below. Alternatively, you could just delete the needed records prior to deleting your organization in code (here's an example of that being done).

    this.schema.alterTable(this.tableName, (table) => {
        table.dropForeign("organization_id");
    
        table
          .foreign("organization_id")
          .references("organizations.id")
          .onDelete("CASCADE")
    });
    Copied!
  • Published lesson Displaying & Copying A Newly Created Access Token

  • Published lesson Deleting/Revoking Access Tokens

  • Replied to Do you have some about "multi-tenant architecture" adonis v6...

    Hi juandiegou!

    I don't have anything on multi tenancy at this time, sorry! It can definitely be done, I've seen some discussions on it, but as a heads up better built-in support is coming down the road in a future AdonisJS version!

  • Replied to Hello,I'm resuming the course, and I already have a question...

    Hi tigerwolf974!

    Vue doesn't like direct mutation of prop values, which is why we're setting the organizationId into it's own ref and syncing it with the organization id from our props.

    However, you're right, it is still redundant and could've been done a little more cleanly using the newer toRef Vue method. The end result in data flow would've been similar though.

    Alternatively, we could've not used two-way data binding on the DropdownMenuRadioGroup, however, then our UI wouldn't have immediately updated but rather only updated after we got the response back from the organization switch. Not a huge deal, since the dropdown is auto-closed after selection, but still nice to be able to see the selection bubble change as the dropdown is closing as confirmation that the switch is occurring.

  • Replied to discussion Is there any point in putting try/catch statements in each method of a controller?

    Unless there are certain endpoints you'd like to do something specific with, in terms of error handling, I would just rely on AdonisJS' exception handling. If you don't have try/catches in your controllers the exception handler will catch any exceptions that are thrown automatically for you.

    You can then update the handler to your liking if you're looking to always return a specific structure for exceptions.

  • Replied to Haha nice what a clean way to solve the issue, I'll replicate...

    Sure thing, memsbdm! Thank you very much for catching this!!
    Just noticed I missed the validation update in my prior comment, will get that edited in now 😊

  • Replied to Well well well, I removed response.header('Referrer-Policy',...

    Oh hey, haha! Seems we both found the culprit around the same time! 😄

    I chose to take the overkill approach in my commit & other comment, though using flash messages instead of a cookie.

  • Replied to Thanks! I will also keep trying to investigate :)

    Alrighty, was able to track down the issue! So this was happening as a side-effect to the Referrer-Policy: no-referrer within reset method of the forgot password controller.

    When we call response.redirect().back(), which happens on our behalf during request validation handling, it reads from the referer header and redirects the user back to that page. With our Referrer-Policy on this particular form set to no-referrer we're telling the browser not to share that referrer with anyone. This is a security step to prevent the token from being leaked via the referrer.

    To properly correct this and also allow any password errors to show, we want to keep the no-referrer designation and instead update how we're handling our validation errors.

    First, let's move the password reset token into a flash message, rather than passing through the form. This will allow us to keep it out of our redirect url should we get any validation errors on submit.

    async reset({ params, inertia, response, session }: HttpContext) {
      // will be in the params if coming from email
      // will be in flash messages if coming from errored validation
      const token = params.value ?? session.flashMessages.get('password_reset_token')
      const { isValid, user } = await VerifyPasswordResetToken.handle({
        encryptedValue: token,
      })
    
      // flash it to the session store
      session.flash('password_reset_token', token)
      // keep this to keep things secure
      response.header('Referrer-Policy', 'no-referrer')
    
      return inertia.render('auth/forgot_password/reset', {
        email: user?.email,
        isValid,
      })
    }
    Copied!

    Then, configure the update method to read the token from the flash message store instead of our form. We'll also want to manually capture and handle the validation error so we can specify where the user should be redirected to, reflashing the token so it continues to be available for us to use.

    @inject()
    async update({ request, response, session, auth }: HttpContext, webLogin: WebLogin) {
      let data: Infer<typeof passwordResetValidator>
    
      try {
        data = await request.validateUsing(passwordResetValidator)
      } catch (error) {
        // manually catch any validation errors that were thrown
        if (error.code === 'E_VALIDATION_ERROR' && 'messages' in error) {
          // reflash the user's password reset token so it is available to use again
          session.reflashOnly(['password_reset_token'])
          // flash the validation errors so they display to the user
          session.flashValidationErrors(error)
          // redirect to the correct page
          return response.redirect(`/forgot-password/reset`)
        }
        throw error
      }
    
      // grab the token from the flash message store to use
      const token = session.flashMessages.get('password_reset_token')
      const user = await ResetPassword.handle({ data, token })
    
      await auth.use('web').login(user)
      await webLogin.clearRateLimits(user.email)
    
      session.flash('success', 'Your password has been updated')
    
      return response.redirect().toRoute('courses.index')
    }
    Copied!

    Next, since our token is no longer in our data, we'll update the ResetPassword action to accept it separately.

    type Params = {
      data: Infer<typeof passwordResetValidator>
      token: string
    }
    
    export default class ResetPassword {
      static async handle({ data, token }: Params) {
        const { isValid, user } = await VerifyPasswordResetToken.handle({ encryptedValue: token })
    
        if (!isValid) {
          throw new Exception('The password reset token provided is invalid or expired', {
            status: 403,
            code: 'E_UNAUTHORIZED',
          })
        }
    
        await user!.merge({ password: data.password }).save()
        await ExpirePasswordResetTokens.handle({ user: user! })
    
        return user!
      }
    }
    Copied!

    Then, since the token will come from our flash message store on any validation redirects and not the route params, we'll make that route parameter optional.

    router.get('/reset/:value?', [ForgotPasswordsController, 'reset']).as('reset')
    Copied!

    Lastly, since we're no longer passing the token to our page or form, we can remove it from our validator.

    export const passwordResetValidator = vine.compile(
      vine.object({
        value: vine.string(),
        password: vine.string().minLength(8),
      })
    )
    Copied!

    With all that, things should be behaving properly! Terribly sorry I missed accounting for that. The no-referrer was something I added in after the fact and failed to recognize it'd have that kind of impact. However, it is definitely something we want to keep.

    You can find the full diff of my commit for this here:
    https://github.com/adocasts/building-with-adonisjs-and-inertia/commit/2f87a54cd84886e6dfffad01568153e2f8fe24a1

  • Replied to Thank you!!! It makes totally sense now 🫶🏼

    Anytime! Awesome, I'm happy to hear it's making sense now!! 😊

  • Replied to I just find out that when trying to reset our password with ...

    Hi memsbdm!

    The mailer at plotmycourse.app is indeed still up and running, you might need to check your spam for it's email. However, I can confirm I'm having the same issue. I'm not immediately sure why this would be happening, it looks like it isn't properly returning an Inertia response from the validation handling.

    I'll be able to dig into it further after work and will let you know if I find anything!

  • Replied to discussion Inertia + React - Change URL but open modal (No full navigation)

    Hey Michel!

    I haven't personally looked into this too deeply of late. I do know the it was once a planned feature within Inertia, but I believe it has since been dropped; though there are many workarounds out there.

    InertiaUI's Modal functionality looks pretty promising though, might be worth checking out!

    A user named Cory on the AdonisJS Discord also just recently shared similar functionality, you might be interested in reading his approach!

  • Replied to It's mostly to share content / blogs / videos that they have...

    Thanks a ton, Validimir!

    I actually don't have WhatsApp and am not all that familiar with it. My current understanding is that it is similar to SMS/text messaging. Please correct me if I'm wrong, but if that is indeed the case I appreciate the invite but will decline at this time. I'm actively on and can be easily reached via the AdonisJS Discord, the Adocasts site, and other social media.

    Beyond that, I like to try and minimize the amount of notifications I receive. 😊

  • Replied to This episode is such a mind blow. I have rolled out custom...

    Happy to hear you enjoyed, Vladimir!! JWT & refresh tokens can definitely be complicated to implement, for sure! 😊

  • Replied to Hey Tom, Thank you for the video » Just wanted to ask a quick...

    Hi Vladimir!

    Yes, there sure is! If you open your JSON user settings:

    1. Hit cmd/ctrl + shift + p to open the command palette

    2. Search for and select "Preferences: Open User Settings (JSON)"

    You can add the following to this file, which will map the EdgeJS file extension .edge to use HTML Emmet functionalities.

    {
      "emmet.includeLanguages": {
        "edge": "html"
      }
    }
    Copied!
  • Replied to Hi Tom, after creating our exceptions like ForbiddenException...

    Hi memsbdm!

    Unless you're looking to handle these exceptions a specific way, the built in status page functionality of AdonisJS should suffice for these! These status pages work the same way in Inertia as they do in standard AdonisJS.

    Any status codes within the range specified via the statusPages object, which is located within your app/exceptions/handle.ts file, will render out the designated page for that status code to alert your user of the exception.

    import app from '@adonisjs/core/services/app'
    import { HttpContext, ExceptionHandler } from '@adonisjs/core/http'
    import type { StatusPageRange, StatusPageRenderer } from '@adonisjs/core/types/http'
    
    export default class HttpExceptionHandler extends ExceptionHandler {
      // ...
    
      /**
       * Status pages are used to display a custom HTML pages for certain error
       * codes. You might want to enable them in production only, but feel
       * free to enable them in development as well.
       */
      protected renderStatusPages = app.inProduction
    
      /**
       * Status pages is a collection of error code range and a callback
       * to return the HTML contents to send as a response.
       */
      protected statusPages: Record<StatusPageRange, StatusPageRenderer> = {
        '404': (error, { inertia }) => inertia.render('errors/not_found', { error }),
        '500..599': (error, { inertia }) => inertia.render('errors/server_error', { error }),
      }
    
      // ...
    }
    Copied!

    If, as shown in your example, you're looking to redirect the user back to the previous page when the exception is raised and alert them via a toast (or something similar) instead. You could create an exception with a handle method doing exactly that, then extend that exception for your ConflictException and any others you've created.

    I believe that'd look something like the below!

    // app/exceptions/exception_redirect_handler
    export default class ExceptionRedirectHandler extends Exception {
      async handle(error: unknown, ctx: HttpContext) {
        ctx.session.flashErrors({ [error.code!]: error.message }) // to access it using errors.E_CONFLICT
        return ctx.response.redirect().back()
      }
    }
    Copied!
    export default class ConflictException extends ExceptionRedirectHandler {
      static status = 409
      static code = 'E_CONFLICT'
    }
    Copied!
    • app
    • exceptions
    • conflect_exception.ts
  • Replied to Hi Tom, Just wondering if you are going to upload the migration...

    Hi memsbdm! I don't have any plans to do so at this point in time, but if there is enough desire for that I'd be happy to circle back. Updating to Inertia v2 was planned and accounted for in the planning of this series, as we knew ahead of time the breaking changes would be next to none and it'd mostly be feature additions. So the additions to the series there wouldn't be overly confusing.

    It does look like Shadcn-Vue now supports TailwindCSS 4, however, that does require updating to their new major version which switches from Radix Vue to Reka UI. They also seem to have dropped support for specifying a tsconfig location in that update.

  • Published lesson Opaque Access Tokens (OAT) vs JSON Web Tokens (JWT)

  • Published lesson Listing an Organization's Access Tokens

  • Replied to Thank you so much! Yeah I'm using "@adonisjs/inertia": "^3.1...

    Awesome, glad to hear all is working now!

    Yep, that'll do it! You'll want to either use:

    • The auth middleware (requires an authenticated user)

    • The silent auth middleware (checks for user, but does not require auth)

    • Or, call await auth.check() to check for an authenticated user without one of the two middleware.

  • Published lesson Creating Access Tokens Part 1: AdonisJS

  • Published lesson Creating Access Tokens Part 2: Inertia/Vue

  • Upvoted comment thank you very much!

  • Replied to thank you very much!

    Anytime!!

  • Replied to // When I do this, I get the errors sharedData: { greetings...

    I'm guessing you're probably using v3 or later of the AdonisJS Inertia adapter and errors is already being shared automatically for you.

    Since you're sharing to a custom errors key in your flash messages, that won't be included within those.

    Try writing to the errorsBag instead. This is how the invalid credentials exception does it itself, and this is automatically checked and populated by the AdonisJS Inertia adapter (v3+).

    if (error instanceof errors.E_INVALID_CREDENTIALS) {
      session.flashErrors({ form: 'Invalid credentials' })
      return response.redirect().back()
    }
    
    // Handle other errors
    session.flashErrors({ form: 'An error occurred during login' })
    return response.redirect().back()
    Copied!

    Then, so long as my assumption is correct and you're using v3 or later of the adapter package, you can remove errors from your sharedData

  • Replied to I want to use a JWT token because I'm considering that the frontend...

    AdonisJS uses Opaque Access Tokens (OAT), sometimes referred to as a by-reference token. Unlike JWTs, with opaque tokens the source of truth is the database and the only thing meant to read the token is the issuing server.

    Since the source of truth is the database, in order for a token to be verified it must go through the server via an introspection endpoint. This also means tokens can be readily revoked at any time. This makes opaque tokens more secure than JWTs, as it follows the never trust, always verified methodology. Since the token can be revoked at any time and doesn't contain any user information they can have a longer expiry than JWT access tokens. However, since it needs to verify the token with the server, it can be less scalable at high user levels.

    Here's more on the matter from AdonisJS' creator.

    That said, if you would like to use JWTs with AdonisJS, you can give MaximeMRF's JWT package a go! That should get you all set up!

  • Upvoted discussion SharedData Problem - Inertia + React

  • Replied to discussion SharedData Problem - Inertia + React

    Hi Michel,

    Are you sure you have an authenticated user and/or errors? Try giving them a console log inside the sharedData just to make sure you actually have a value there. This has been the reason for a few others in the past.

    sharedData: {
      greetings: 'bonjour',
    
      user: (ctx) => {
        const user = ctx.auth?.user
        console.log({ user })
        return user
      },
    
      errors: (ctx) => {
        const errors = ctx.session?.flashMessages.get('errors')
        console.log({ errors })
        return errors
      },
    }
    Copied!

    Also, if you're using v3 of the AdonisJS Inertia adapter or later, you don't need to manually pass the errors via sharedData, the package will now do that automatically.

  • Replied to Thank you, Tom Gobich. Absolutely brilliant course! I spent ...

    Thanks so much, codthing!! I greatly appreciate that! I'm really happy to hear everything was easy to follow even with using a translator!

  • Upvoted comment thank you tomgobich

  • Replied to thank you tomgobich

    Anytime! Thank you, again, for the great suggestion!

  • Replied to The issue lies in the fact that only English letters are supported...

    Ah, I'm happy to hear you were able to track down the root cause! Sorry there wasn't a warning in-video about that, codthing! Locales on the slugify didn't even occur to me.

  • Published lesson Configuring Access Token Auth on top of Session Auth

  • Published lesson Separation of API & Web Auth Guard Concerns

  • Published lesson Defining Access Token Abilities & DTO

  • Replied to Thanks i will go through that lesson and update you

    Sounds good, Rajeeb! Hope it helps!! 😊

  • Upvoted comment Thanks, I'll give it a try

  • Replied to Thanks, I'll give it a try

    Anytime! Best of luck!! 😊

  • Replied to If the GitHub repository includes the relationship diagram from...

    Hi codthing!

    Thank you for the great suggestion! You can now find the database diagram within readme of the repository and the repository's public directory!

  • Upvoted discussion Adonis + Docker

  • Replied to discussion Adonis + Docker

    Hi ab-ab!

    I don't personally use Docker, so this isn't something likely to be covered on Adocasts anytime soon. You can give AdonisJS Sail a go though, I believe this is meant to help with getting a Docker environment set up in an AdonisJS 6 app.

  • Upvoted discussion About packaging in adonisjs + inertia

  • Replied to discussion About packaging in adonisjs + inertia

    Hi Rajeeb!

    That would definitely be something that's possible, though we don't have a lesson on that specific topic here at this time. We do, however, have an introductory lesson on how to make an AdonisJS package that may help get you started.

    With Inertia reaching both the front & back end, though, the scope of what your package would look like will depend on whether it is intended for the front end, back end, or both.

  • Published lesson Goal of this Series

  • Published lesson Getting the Web Project Up & Running

  • Published lesson Getting Familiar with our Web Project

  • Published lesson Overview of our Database Schema

  • Replied to Hi Tom, the project I'm working on to learn how to use Adonis...

    Hi n2zb! You can preload pivot table data two ways.

    First option is to define them directly in the relationship definition, as shown in this lesson at 6:50. This will always include them when the relationship is preloaded. So, this is a great option is you'll frequently need that pivot data.

    export default class Movie extends BaseModel {
      // ...
    
      @manyToMany(() => Cineast, {
        pivotTable: 'cast_movies',
        pivotTimestamps: true,
        pivotColumns: ['character_name', 'sort_order'], // 👈
      })
      declare castMembers: ManyToMany<typeof Cineast>
    
      // ...
    }
    Copied!

    The second option is to add them on a per-query basis, as needed.

    const crew = await movie
      .related('crewMembers')
      .query()
      .pivotColumns(['title', 'sort_order']) // 👈
      .orderBy('pivot_sort_order')
    Copied!

    This will then add those pivot columns into the $extras object of the preloaded relationship.

    const crewMember = {
      // ...
    
      '$extras': {
        pivot_title: 'Art Director', // 👈
        pivot_sort_order: 0, // 👈
        pivot_movie_id: 417,
        pivot_cineast_id: 40,
        pivot_created_at: DateTime { ts: 2024-03-30T11:59:58.911+00:00, zone: UTC, locale: en-US },
        pivot_updated_at: DateTime { ts: 2024-03-30T11:59:58.911+00:00, zone: UTC, locale: en-US }
      },
    }
    Copied!

    Note that the column names are prefixed with pivot_ and that the ids of the relationship are always included. Also note, if you're serializing your models, you'll want to turn on extras serialization via the below on the model, and they'll be serialized as meta.

    export default class Cineast extends BaseModel {
      serializeExtras = true
    
      // ...
    }
    Copied!

    We get into all of this in various later lessons in this series as well, if you'd like a deeper dive!

  • Replied to After testing with these two route methods, there were no changes...

    Hi codthing! Everything you've shared looks right. If you're viewing your Redis database with a GUI application, those often don't reflect live data and require refreshing in order for them to reflect changes made to the database contents. Also, make sure you're viewing the Redis database actually being used by your application! This can be found within config/redis.ts

    const redisConfig = defineConfig({
      connection: 'main',
    
      connections: {
        main: {
          host: env.get('REDIS_HOST'),
          port: env.get('REDIS_PORT'),
          password: env.get('REDIS_PASSWORD', ''),
          db: 1, // 👈 indicates I'm using Redis DB1, DB0 is typically the default
          keyPrefix: '',
          retryStrategy(times) {
            return times > 10 ? null : times * 50
          },
        },
      },
    })
    Copied!
    • config
    • redis.ts

    You might also do a sanity check, just to ensure your controller methods are actually being hit correctly, by adding a console log to each.

    //redis controller
    export default class RedisController {
        public async destroy({ response, params }: HttpContext) {
            console.log('destroy', params.slug)
            await cache.delete(params.slug)
            return response.redirect().back()
        }
    
        public async flush({ response }: HttpContext) {
            console.log('flushing redis')
            await cache.flushDb()
            return response.redirect().back()
        }
    }
    Copied!

    If neither of those help, please feel free to share a link to your repository and I can take a deeper look to see if anything stands out!

  • Replied to Thanks Tom. Thats what i am seeking for. I will find way to ...

    Anytime! Okay cool, yeah updating the logger destination and logging the queries, as shown in the last two code blocks in my comment above, should get that working for ya!

  • Replied to How can i log the query to check if i am writing query correct...

    Hi Rajeeb!

    Console logging a call to toSQL() as you have should work, you should be seeing something like the below in your console.

    However, there is a nicer way to do this, using pretty printed query logging. Within your config/database.ts, set prettyPrintDebugQueries: true.

    const dbConfig = defineConfig({
      prettyPrintDebugQueries: true, // 👈
      connection: 'postgres',
      connections: {
        postgres: {
          client: 'pg',
          connection: {
            host: env.get('DB_HOST'),
            port: env.get('DB_PORT'),
            user: env.get('DB_USER'),
            password: env.get('DB_PASSWORD'),
            database: env.get('DB_DATABASE'),
          },
          migrations: {
            naturalSort: true,
            paths: ['database/migrations'],
          },
        },
      },
    })
    Copied!
    • config
    • database.ts

    Then, you can either globally enable query logging, by adding a debug: true to your driver object.

    const dbConfig = defineConfig({
      prettyPrintDebugQueries: true,
      connection: 'postgres',
      connections: {
        postgres: {
          debug: true, // 👈
          client: 'pg',
          connection: {
            host: env.get('DB_HOST'),
            port: env.get('DB_PORT'),
            user: env.get('DB_USER'),
            password: env.get('DB_PASSWORD'),
            database: env.get('DB_DATABASE'),
          },
          migrations: {
            naturalSort: true,
            paths: ['database/migrations'],
          },
        },
      },
    })
    Copied!
    • config
    • database.ts

    Or, you can enable this this on a per-query basis by adding debug(true) to the query.

    await Collection.query().where('slug', params.slug).debug(true).firstOrFail()
    Copied!

    With that set, you should see something like below in your console

    Now, unfortunately, this pretty print can't be logged to a file. You can still log the query to a file though. Within your config/logger, set the non-production logger target to a file at the destination you'd like. If that's a file in storage, it'd look something like:

    const loggerConfig = defineConfig({
      default: 'app',
    
      loggers: {
        app: {
          enabled: true,
          name: env.get('APP_NAME'),
          level: env.get('LOG_LEVEL'),
          transport: {
            targets: targets()
              // 👇
              .pushIf(!app.inProduction, targets.file({ destination: './storage/local.log' }))
              .pushIf(app.inProduction, targets.file({ destination: 1 }))
              .toArray(),
          },
        },
      },
    })
    Copied!
    • config
    • logger.ts

    Then, add a global listener for the query within a preload.

    node ace make:preload events
    Copied!
    import emitter from '@adonisjs/core/services/emitter'
    import logger from '@adonisjs/core/services/logger'
    
    emitter.on('db:query', function (query) {
      logger.debug(query)
    })
    Copied!
    • start
    • events.ts

    With this all your logs, including queries, will be written to the file at the location you've specified! Hope this helps!! 😊

  • Replied to Having worked exclusively with Express and EJS on the backend...

    That's awesome to hear, Vladimir! I'm really happy to hear you're finding the lessons early to follow and enjoying them, thank you for sharing!

  • Replied to Thank you so much!! I tried many tags without success but tbody...

    Anytime! Awesome, I'm happy to hear that got things working for you, memsbdm!!

  • Published lesson How to Install & Configure TailwindCSS 4 in AdonisJS 6 using Vite

  • Upvoted discussion Vue Sortable

  • Replied to By the way I am trying to use &lt;Sortable&gt; inside a &lt...

    Hey memsbdm! Yeah, the deployed app has been expanded beyond the scope of the Building with AdonisJS & Inertia series. It's what I use to plan lessons and series, and also feeds the data shown on our schedule page. So, I've added some things since. Apologies if that caused any confusion!

    I did have the repository private, but just made it public. You can find the code for the live PlotMyCourse app here.

    The <Sortable> component accepts a tag prop, and I have that set as tbody so that it plays well structurally with the table itself. I think that might be all you're missing!

    <Table>
      <TableHeader>
        <TableRow>
          <!-- ... -->
        </TableRow>
      </TableHeader>
      <Sortable
        v-if="courses?.length"
        v-model="courses"
        item-key="id"
        handle=".handle"
        tag="tbody" <!-- 👈 -->
        class="[&_tr:last-child]:border-0"
        @end="onOrderUpdate"
      >
        <template #item="{ element: course }">
          <TableRow class="group">
            <!-- ... -->
          </TableRow>
        </template>
      </Sortable>
    </Table>
    Copied!
  • Upvoted comment perfect, thanks :)

  • Replied to perfect, thanks :)

    Anytime, Nidomiro!! 😊

  • Upvoted comment Worked fine. Thank you!

  • Replied to Worked fine. Thank you!

    Awesome, that's great to hear!
    Anytime!!

  • Replied to In the file inertia/composables/resource_action.ts, this line...

    Hi memsbdm!

    It looks like this was a tiny change that occurred between InertiaJS v1 and v2! In this lesson, I'm still working with Inertia v1, which is why everything works without issue for me. We upgrade to InertiaJS v2 in module 13, though this is one change I missed!

    Here's what the FormDataType looks like in InertiaJS v1:

    And then here is what it is now in InertiaJS v2:

    So, your solution is perfect for v2! You could probably try and use `Record<string, FormDataConvertible> if you'd prefer to be as accurate as possible! Though I don't believe what you have should cause any issues down the road! Thank you for sharing! 😊

  • Replied to Thank you, I had missed that information in the documentation...

    Anytime and no worries at all! That's completely understandable!! 😊

  • Replied to Hi, thanks for creating the series. I'm used to creating folders...

    Hi Nidomiro!

    Yep, that's absolutely a valid approach with AdonisJS! I haven't done it myself, but you can check out Romain Lanz's website which uses this setup. He is one of the AdonisJS Core Members.

    You'd need to move some files around to your liking manually, then update the imports within your package.json to match your feature names (if you want to use import aliases) rather than having one for controllers, models, etc.

  • Replied to discussion building-with-adonisjs-and-inertia repository setup error on local machine.

    Hi Thiago!

    Sorry you're having issues! I think I found the culprit. It looks like TailwindCSS was having difficulty picking up the default configuration to use. Explicitly defining the config seems to have fixed the issue - at least on my end, but I couldn't get the error to consistently happen.

    Please let me know if this remedied the issue for you!

    Thanks and sorry again you're having issues!

  • Published lesson Dump & Die Debugging in AdonisJS 6

  • Upvoted comment Awesome! Thank you!

  • Replied to Awesome! Thank you!

    Sure thing, Aaron!!

  • Replied to Hello, In this video, you are using a transaction with db.transaction...

    Hi Tigerwolf!

    Lucid cascades the transaction to relationship operations for us! So, when we attach the transaction to the operation that creates the organization, the transaction reference will be kept in the organization instance.

    return db.transaction(async (trx) => {
      const organization = await Organization.create(data, { client: trx })
      
      organization.$trx // 👈 holds the transaction
    
      // ...
    })
    Copied!

    This transaction reference is then automatically used when we perform subsequent operations with this organization, which includes related operations.

    return db.transaction(async (trx) => {
      const organization = await Organization.create(data, { client: trx })
      
      // 👇 passes $trx on the organization along with the attach db operation
      return organization.related('users').attach({
        [user.id]: {
          role_id: Roles.ADMIN,
        },
      })
    
      // ...
    })
    Copied!

    The managed transaction, then, wraps the callback within a try/catch block. The transaction is committed if the callback completes and no errors are thrown. Otherwise, if an error is caught, the transaction is rolled back. Essentially doing the below for us in a nicer syntax.

    const trx = await db.transaction()
    
    try {
      const organization = await Organization.create(data, { client: trx })
      
      // 👇 passes $trx on the organization along with the attach db operation
      return organization.related('users').attach({
        [user.id]: {
          role_id: Roles.ADMIN,
        },
      })
    
      // ...
    
      await trx.commit()
    } catch (error) {
      await trx.rollback()
    }
    Copied!

    So, as long as we don't eat the errors thrown by the operations we're performing, the managed transaction will catch it and roll back our transaction. This is regardless of how we structure our code within the managed transaction, including splitting it into additional methods.

    So, to answer your question, If an issue occurs in assignAdmin or createDefaults the managed transaction will catch this and roll back our transaction for us! You can see this for yourself by trying to assign the user a role_id that doesn't exist. Since we're using a foreign key constraint here, attempting to do so will throw an error.

    static async #assignAdmin(organization: Organization, user: User) {
      return organization.related('users').attach({
        [user.id]: {
          role_id: 45, //Roles.ADMIN, // 👈 replace admin with some non-existant id
        },
      })
    }
    Copied!
  • Completed lesson Onboarding Newly Registered Users

  • Replied to Thank you very much! Your answer solved my problem.

    Anytime! Awesome, glad to hear you’re all set Oliver!

  • Upvoted discussion Passing parameters to the router line

  • Replied to discussion Passing parameters to the router line

    Hi Oliver!

    Partial route parameters, like /fly-to-:city aren't supported by the AdonisJS Router at present. However, you can use a vague route parameter in conjunction with a route param validator to make this work.

    router.get('/:flyToCity', [Controller, 'handle']).where('flyToCity', /^fly-to-/)
    Copied!

    The where here is applying a validator to the route parameter, stating that the parameter should start with "fly-to-" in order for the route to match the definition.

    We have a lesson where we cover this using user profiles in our Let's Learn AdonisJS 6 series if you'd like to learn more!

  • Replied to thank you so much, I've been able to set it up. Thank you so...

    Awesome, I'm really happy to hear you're all set up now! Thanks a bunch, Ikeh!! 😊

  • Replied to No worries at all! It's been a long while since I've used MySQL...

    Alrighty, so I took a look into it. I was also getting the error with MySQL, and it was indeed a code issue! I accidentally missed binding the organization to the transaction, which caused the decrement to run outside the confinement of the transaction - hence the lock block.

    Below is the updated code that fixes this issue! Terribly sorry about the miss here! I'll get a note about this added into the lesson.

    export default class DestroyLesson {
      static async handle({ organization, id }: Params) {
        const lesson = await organization.related('lessons').query().where({ id }).firstOrFail()
    
        await db.transaction(async (trx) => {
          lesson.useTransaction(trx)
          organization.useTransaction(trx) // 👈
    
          await lesson.delete()
          await organization
            .related('lessons')
            .query()
            .where('moduleId', lesson.moduleId)
            .where('order', '>', lesson.order)
            .decrement('order')
        })
    
        return lesson
      }
    }
    Copied!
  • Replied to discussion Not able to share flash messages to frontend with inertia

    Hi Deniz!

    I think you've got a small typo. You're flashing to notification in your controller, but sharing notifications. You'll want those two to match 1:1.

    Also, make sure you've got the flash message key notification or notifications (whichever you pick) defined as a prop on your page/layout. Otherwise Vue, for example, won't ingest it and will treat it as an attribute and not a prop.

  • Replied to Thanks! I found it. A typo… was missing the in organization....

    Anytime! Awesome, I'm glad to hear you were able to get it fixed up! As a morning person, I can concur! 😄

  • Replied to Sorry for all the questions. I am getting a lock timeout error...

    No worries at all! It's been a long while since I've used MySQL, so my platform-specific knowledge here will be limited. But, if it works fine without the decrement then I'd imagine you most likely have a stuck lock or transaction on one of your lessons. Something not directly related to the decrement, but rather blocking the decrement from succeeding.

    This may or may not be code-related. It could be a lock added by a client application or CLI using your MySQL table. It could also be another transaction touching one of the lesson rows that was never committed or rolled back. Here is a StackOverflow thread that discussed some query options to dig into what might be locking things up.

    I can try and get MySQL set up and test it out this evening after work to ensure it isn't a code issue specific to MySQL.

  • Replied to finding it difficult to install Redis on Windows. any alternative...

    Hi Ikeh! Redis is used for a very brief period in this series. I believe 2.14 and 2.15 are the only two lessons we use it within, just to show how to work with it. It is completely skippable if you don't feel like setting it up on your machine!

    Alternatively, you can use Redis Cloud. They offer a small free tier if you click "Try for Free" and then select the "Essentials" plan option during sign-up, as shown below.

    Once you've created your database, you can click on "connect" and then expand the "Redis Client" section. Here you can copy/paste the username, password, host, and port into your .env file of your project.

    Note: you have to click the "copy" button to get the actual password

  • Replied to Yeah I finally tried to recreate the GetOrganizationAbilities...

    That's awesome! I'm happy to hear you're making progress with it! I wouldn't worry too much about doing things the best or cleanest way, especially initially! So long as it works for what you need in a way you understand, that's all that matters to keep progress going. You can always refactor down the road if need be. 😊

  • Replied to Thank you! Yes, I found from another forum that I was not using...

    Awesome! I'm glad to hear you were able to get it figured out! 😊

  • Replied to discussion shared data in inertia config is not coming into vue props

    Hi Ryan!

    The authenticated user isn't populated until you tell AdonisJS to populate it. This can be done using the auth middleware, silent_auth middleware, or manually calling auth.check() as needed. You're most likely missing one of those.

    Note that you also need to specify a serialized type for your Lucid Models using either casting or DTOs.

  • Replied to I went with the row level with model hooks. Works well, I had...

    Yeah, you'll want the HttpContext here to be optional. Then, when you don't have an HttpContext, you'll want to signal to yourself that your permissions haven't been populated; leaving it null there could be a way to do that.

    @afterFind()
    static async attachPermissions(workspace: Workspace) {
      const ctx = HttpContext.get()
    
      if (!ctx) return
    
      // ...
    }
    Copied!

    If the HttpContext isn't optional then your model would also become unusable within commands, jobs, events, and all that fun stuff. In most cases, you shouldn't need permissions on those types of requests. On the off chance you do, that's where you'd want to extract the contents of the hook into a model method, service, or action so you can also use the same logic on an as-needed basis.

  • Replied to The line statusId: props.organization.statuses.at(0)?.id works...

    Hi Aaron!

    It sounds like your organization is undefined within the SortableLessons component. If it works fine within SortableModules then it should be populated on the page okay. Make sure you've got it within the props being passed into SortableLessons!

    <SortableLessons
      v-model="modules[index]"
      :organization="organization" <!-- 👈 -->
      :course="course"
    />
    Copied!

    If it's there, then make sure it is within SortableLessons props.

    const props = defineProps<{
      organization: Organization // 👈
      course: CourseDto
      modelValue: ModuleDto
    }>()
    Copied!

    I'd reckon it's one of those two, but if it is in both of those spots, I'd recommend using the Vue DevTools to inspect the flow of this prop to see where it might be getting lost.

    Hope this helps!!

  • Replied to just saw this now after my complaint. thank you

    Oh shoot - I recorded these updates and got the Inertia one updated but missed this one. I'll get this updated after work!

  • Upvoted comment Thank you! 🎊🎉🥳

  • Replied to Thank you! 🎊🎉🥳

    Thanks so much for watching, cortesbr!! ❤️

  • Replied to Hi again Tom ! Sorry I ask a lot of questions 😅I was wondering...

    Hi Emilien!

    No worries at all!! 😊

    Yeah, things spiral here quickly depending on your approach. It's a big enough of a topic to be a series of its own, but I'll try to give an overview below.

    Access Determination

    So, the first thing you need to do is figure out how the more granular access will be determined. Will only the creator have permission to edit, update, and delete? Or, maybe the creator can invite others to edit, update, and delete? Essentially, how are the different roles going to be determined at those different levels?

    Creator Only

    If it's just the creator, then that's much easier. You'd want to capture the userId of the user who created the resource. For example, adding a ownerId onto the Course model. Then, you'd handle the logic very similarly to how we allow org members to remove themselves throughout the next two lessons (12.1 & 12.2). By giving priority to allow the action if the ownerId matches the authenticated user's id. When that's the case, you'd want to ignore whatever the role's permission says and allow the action.

    Resource Specific Invitations

    If you want to allow the creator to invite/share with others to collaborate, you'll need a table to track that. This table could look many different ways.

    • It could be a single table per resource, ex: course_user consisting of course_id, user_id, and role_id if there are different levels and not an on/off.

    • A polymorphic table, ex: permission_user consisting of something like entity , entity_id, user_id, and again role_id if needed. Here, entity would be the table/model and entity_id would be the row's id for that table/model. Now, Lucid doesn't support polymorphic relationships but that doesn't mean you couldn't specifically query for what you need. The downside is, that you can't use foreign keys for the entity_id since it'll be dynamic

    • A single table with a nullable column per resource, ex: permission_user consisting of something like course_id, another_id, user_id, and again role_id if needed. This is similar to the polymorphic relationship, but you have a specific column per resource needed. Plus here, is you can keep foreign keys. Downside here, is it can be a lot of columns if a lot of resources need to use it.

    There are many different ways authorization can look beyond these two, but determining what that looks like is the first step.

    Populating Permissions

    When it comes to populating these permissions, again, it's going to depend on just how granular you need to go. For us in this series, things were simple so our approach was simple. The approach covered here was meant mostly for a single set of roles that then trickle down as needed.

    If you're going to be listing courses and each of those courses in the list is going to have varying permissions based on invites/shares, then you might be better off moving the can approach to the row level. For example, allowing something like course.can.edit to determine if the authenticated user can edit the course. This could be done using something like model hooks.

    You could also always reach for a package to help achieve your implementation.

    That was a lot, sorry! Hopefully, that helps at least little though.

  • Replied to Thanks for your effort. I will find out issue and let you know...

    Sorry I couldn't be of more help, Rajeeb! I hope you're able to find a solution. You might have luck reaching out on the AdonisJS Discussions or Discord.

  • Replied to Unbelievable !!! :-O I wanted to sincerely thank you for your...

    My pleasure, tigerwolf!! Happy to be able to help! 😊

  • Replied to Hello,First of all, thank you for the high-quality content you...

    Hello, tigerwolf!

    Sorry about the inconvenience!! Thankfully, the video host we're using for these videos allows specifying custom CSS within the player's iframe, and I was able to get the captions pushed up when the video toolbar is shown. I also hid the heatmap, which seems to be mostly what was going over the subtitles.

    We're in the process of switching from an actual video host to Cloudflare R2 with a hand-picked player, so hopefully we'll have more improvements in this area down the road!

    Thank you very much for the feedback!!

  • Replied to Hi, thank you for this content. Unfortunately , subtitles are...

    Terribly sorry about that, tigerwolf! The subtitles for this lesson have now been corrected. Thank you very much for the heads up!

  • Replied to discussion Recently from today i am getting error on node package on adonisjs

    Hi Rajeeb!

    Do you have ts-node-maintained installed? This package is a version the AdonisJS Core team support that's a maintained fork of ts-node.

    Please make sure that this package is installed and that you're attempting to run your Ace CLI command from the correct directory. If in development, that'll be the root of your project. If you're attempting to run a built application, that'll be in the build directory. I'm unfamiliar with Docker, but that may be a source of the issue as well.

    As a sanity check, I just pulled down a new installation of the AdonisJS Inertia starter kit and it installed and booted successfully.

  • Replied to Hi Tom,I actually managed to make it work by reporting an custom...

    Hi emilien!! I think your linked repo is private, but I'm happy to hear you were able to find a solution! I just took a look into it and the target was email.database.unique where email is the validator field's name.

    The below, within resources/lang/en/validator.json, worked for me!

    {
      "shared": {
        "messages": {
          "email.database.unique": "This is from the translation"
        }
      }
    }
    Copied!

    You can find the logic used to find a translated message within the I18nmessagesProvider . Also, my initial thinking was incorrect. This is actually used immediately by the validator, not inside the middleware!

  • Published lesson Defer Loading Props in InertiaJS 2

  • Published lesson Deferring A Prop Load Until it is Visible in InertiaJS 2

  • Published lesson Super Easy Infinite Scroll in InertiaJS 2 with Prop Merging

  • Replied to Hi Tom, I was wondering how we can set custom error messages...

    Hi emilien!

    I actually haven't used the i18n AdonisJS package at all yet, probably should familiarize myself with it though haha.

    The field target should just be email.unique, I believe. If you're trying to use the Detect User Locale middleware to automatically do this, it might be possible that the Inertia middleware is committing it's session store before the Detect User Locale middleware is able to apply the translations. Playing with the middleware order there, having inertia before the Detect User Locale inside start/kernel.ts, might help.

    You could also try passing it directly into the validate method, as shown here.

    When I have some free time, I'll play around with it a bit to see if I can get it figured out!

  • Replied to Hi there! I’m currently using adocasts.com and I’ve actually...

    Thanks so much for sharing, cortesbr!! I believe this is either a browser or extension issue. After closely inspecting, I think the page is actually still there & active, the browser just isn't showing it to you for some reason.

    You do click several places, but at 0:07 you click somewhere that actually has an anchor link. At 0:10 the site comes back and briefly shows the linked anchor that was clicked at 0:07 is "Allowing Admins to Update Movies and Clear Values". Moments later the page for that clicked lesson loads.

    I've been trying for the past half hour or so to replicate this on my Windows machine using Edge, and haven't had any luck. Browsers, Edge included, have a number of battery/resource saving features like putting inactive tabs to sleep. This could be one cause - though I tested several Edge settings and couldn't replicate. Extensions are also common sources of issues.

    I'll keep an eye out and try replicating further, but in the meantime I'd recommend giving it a go in a private/incognito window to see if that fixes the issue you're having. If I had to place a wager, I'd guess it's an extension.

  • Replied to Thanks. Just a note, I tried npx shadcn-vue@radix add table ...

    Looks like npx shadcn-vue@radix is targeting 0.11.4, just as an alias, so those should behave the same. It's possible there was a network issue - their add commands send API requests out to the actual ShadCN site to get configurations, like colors. Based on this issue, you might also have an invalid baseColor in your components.json.

    Here's my attempt to run it and you can see it prompted me to install v0.11.4

    The unsupported engine warning you got is stating that the package undici requires NodeJS v20.18.1 or higher, but you have v20.11.0 installed. So, that'll go away if you update your NodeJS version, not entirely sure what makes use of undici.

  • Replied to It looks like shadcn updated recently to use Reka instead of...

    Yep! I'm working on updating lesson 1.4 of this series this weekend just for this, though in the updated lesson, I'll recommend sticking with Radix for the time being. Getting their Reka update working is too much of a change, and there may be several inner-component changes as well that would confuse those following this series. You can still access the documentation for the Radix version at radix.chadcn-vue.com. You can also target it via their CLI via:

    npx shadcn-vue@radix <command>
    Copied!

    I've tried getting their Reka updating working myself, and tried re-initializing - neither work without compromise. They dropped support for specifying a tsconfig.json location and will instead only check the project root for a tsconfig.json, tsconfig.web.json, or tsconfig.app.json file in that order. They have a preflight check on their CLI commands that will fail the command if the found tsconfig is missing an alias definition, which is also why it states the components.json file is incorrect despite it matching their spec.

    So the only possible way to get their Reka UI update working would be to rename the AdonisJS tsconfig.json file in the root of the project to something else, as Shadcn-Vue's preflight command check will only ever find and use this tsconfig file due to its name and priority in their search.

    You'd then need to move your Inertia tsconfig.json file out into the project root.

    I haven't tried, but you may have luck getting things working if you move all the Shadcn-Vue stuff into the inertia folder.

  • Upvoted discussion Delayed Page Rendering on Tab Focus

  • Replied to discussion Delayed Page Rendering on Tab Focus

    Hi cortesbr! I'm not sure I'm fully following. Would you mind providing a little more context, please?

    For example, are you experiencing this behavior:

    1. On the actual Adocasts site here at adocasts.com?

    2. On a cloned Adocasts site locally on your machine?

    3. As part of the code from the Let's Learn AdonisJS 6 series in the linked lesson?

    Additionally, when you say the page initially appears blank - is this completely blank as in nothing at all shows, or is it just a certain section of the page? If it is just a certain section, which section would that be?

    Thanks in advance!!

  • Replied to Thanks for your answer, I was able to register some users properly...

    Anytime! Awesome, great to hear things are working for you now!
    No worries at all, always happy to help where I can! 😊

  • Replied to Thanks for guidance. Now it worked.

    Anytime, Rajeeb! Awesome, great to hear!

  • Published lesson Upgrading to Inertia 2

  • Published lesson Polling for Changes in InertiaJS 2

  • Published lesson Prefetching Page to Boost Load Times in InertiaJS 2

  • Completed lesson Upgrading to Inertia 2

  • Replied to i am stuck on initializing shadcn-vue /var/www $ npx shadcn-...

    Hi Rajeeb! Apologies about that, it looks like Shadcn-Vue changed their init command to require more manual work to get things set up. Please try initializing using the latest 0 version with:

    npx shadcn-vue@0 init
    Copied!

    I'll have to compare to see what/why they changed it, and I will update this lesson accordingly!

    Edit: Okay, so it looks like in v1 they've switched the default core library from using Radix-Vue to Reka-UI. Using the below to initialize your Shadcn-Vue installation is now the recommended command to continue using Radix, which is what this series uses.

    npx shadcn-vue@radix init
    Copied!

    With this command, this installation should match that covered in this lesson. I'll get this lesson updated accordingly.

  • Replied to Hello, Tom,I see in the LucidModel interface that there is a...

    Hi n2zb!

    I have never tried changing the table property at runtime like you're asking about. I imagine it would work, but even if it does, it would be susceptible to accidentally entering data into the wrong table.

    Instead, I'd recommend defining those shared columns & relationships via a Model Mixin and then composing the mixin into a separate model per table. I thought I had a free lesson on this, but it looks like there is currently only this one from the Building with AdonisJS & Inertia series. I'll have to make a free one on this as well, as it's quite handy.

    In the meantime, you can get the gist of it via the code from this series.

  • Replied to Hello, I did few steps till 05:10 and got an error ( Unexpected...

    I'm not getting that exact error, but I see a couple of issues that'll cause things not to work correctly.

    First, for your form errors, you want to use $messages not $message

    <label>
      <span> Full Name </span>
      <input type="text" name="fullName" value="{{ old('fullName') || '' }}" />
      @inputError('fullName')
        {{ $messages.join(', ') }} {{-- 👈 use $messages --}}
      @end
    </label>
    Copied!

    Second, your full name input has the name fullName with a capital N while your validator has fullname, these need to match 1:1. With those mismatched and the field required by your validator, you'll always get a validation error on submission.

    export const registerValidator = vine.compile(
      vine.object({
        fullName: vine.string().maxLength(100), // 👈 use fullName
        email: vine.string().email().normalizeEmail(),
        password: vine.string().minLength(8),
      })
    )
    Copied!

    Hope this helps! If, after these fixes, you're still getting the state error, please let me know which page and operation you're performing that's causing that error.

  • Replied to I've tried to make a flowchart of the middleware and how it ...

    Oh nice! Yeah, this looks spot on, nicely done emilien! Thank you, I really appreciate that offer! I'm happy to hear that going through this was fruitful!! 😃

  • Replied to Hi. Anyone having issues with npx tailwindcss init -p, just...

    Yeah, I should probably add a note in this video for that. npx tailwindcss init -p is a TailwindCSS 3 command. TailwindCSS 4's default installation doesn't include a tailwind.config.js file anymore and comes with its own Vite plugin for simplified configuration.

  • Replied to Is AdonisJS set up to handle soft deletes? If you didn't want...

    Hi Aaron! Unfortunately, not out of the box. Looks like there is a community package to add support for it, though. An alternative approach I've seen discussed is to have a soft_deletes table that contains a serialized version of the deleted row with additional columns specifying its original table & id. You'd then delete the record, serialize it, then add it to the soft_deletes table so that you can still grab it and restore it if needed. I've never attempted this approach, but it does sound interesting, so I thought I'd share. Not sure how this would handle foreign keys pointing to the deleted id though.

    Yes, sorry about that, those should be optional props!

  • Replied to I think that prettier-ignore-start only works for markdown files...

    Hi Nathan! Huh - yeah it sure does. 🤔

    Oh snap, looks like I disabled Prettier formatting on save for TypeScript files at some point. So, Prettier just isn't running for me unless I paste or explicitly format, sorry about that! Your solution does look to be the recommended approach. I'm going to have to re-enable Prettier and add that myself. Thank you for sharing!!

  • Replied to Great! I see you're already working on this for the next lesson...

    Yeah, we'll add a fix for this at the end of lesson 13.0! 😊 Thanks again, jals!!

  • Replied to Of course this helps, could have spent a lot of time on this...

    Awesome, glad to hear that helped clear things up! Ah, please don't feel dumb about that, it is just a small oversight. We've all done a lot worse!! 😊

    That sounds like a great plan! Having a document/diagram of how things work would definitely help. That's something I'd like to make a more concerted effort to do in-lesson.

    Anytime emilien!! Always happy to help where I can! 😊

  • Replied to You should receive organizationId in the route and validate ...

    Hi jals! That's a great point and a use-case that slipped my mind, thanks for catching that!

    Adding the organizationId into the route parameters is definitely a great option. If you add the organizationId into the URL though, I might prefer to do that across the board and drop the active_organization cookie altogether. That way you only have one source of truth on which organization is being worked with. Minimizing potential confusion down the road.

    You could, alternatively, use a Broadcast Channel to communicate across the tabs to notify the others when the organizationId has changed. You could then display a modal warning the user, offering to re-up their active org. You could also add an event when the tab is focused to update, set, or check the active organizationId. Something like the below would probably suffice.

    onMounted(() => window.addEventListener('focus', onFocus))
    onUnmounted(() => window.removeEventListener('focus', onFocus))
    
    function onFocus() {
      router.get(`/organizations/${props.organization.id}`)
    }
    Copied!
  • Replied to Hi Tom, thanks for your reply. After reading my first comment...

    Alrighty, I think I see what's going on! Where your project differs is that your /workspaces/:id route merely renders a page. In our project, and even in your main branch, this instead sets the active org/workspace in our PlotMyCourse & on your main branch, then redirects back to the dashboard.

    Where I think the confusion comes in is the /:id in your /workspaces/:id route is merely symbolic since you're actually loading the workspace via the user's cookie. So, the displayed workspace is whatever is stored in the cookie and not what is provided via the route parameter id. Things do seem to still be working, the issue is that the id in the url can differ from what is actually displayed. Which, can lead to issues if you were to ever actually use the url's id for something.

    So, to prevent that possibility, I would either

    1. Stick to the cookie and get rid of the /:id in your /workspaces/:id route to instead have something like: /workspace or /workspaces/show

    2. Or, drop the cookie and instead use the id route parameter.

    To add, there isn't an issue with using /workspaces/:id/active to set the active workspace. The issue is that the new /workspaces/:id route that shows the workspace has two spots it could pull the id from; the id route parameter in the url and the cookie, and those can differ from one another.

    Here's a video walkthrough of the behavior I was seeing on your feat/workspace-roles branch. This shows how user-a could request workspace b via /workspaces/2 but actually still saw workspace a; meaning the id in the url did not match the workspace used.

    Here's another walkthrough of the behavior I saw on your main branch. In this branch everything works fine.

    Hope this helps!!

  • Replied to Hi! I'm trying to add a new confirmPassword field only for validation...

    Hi usources!

    Yeah, what you're doing there is perfectly valid, and what I do in most cases where I don't need to omit a ton of fields. Alternatively, you could use an omit utility, like what is offered from Lodash, but what you have is absolutely fine!

  • Replied to Hi Tom, I'm currently building a task management application...

    Hi emilien! I think I'm following correctly, but let me know if this sounds off base.

    So, we manage this with our organizations by querying the active org through the authenticated user, which ensures the user is a member of the org. If the user sets an id of an org that they aren't a member of, our query won't find a match and will instead overwrite the user's active org to the first org found that they're a member of.

    For our application, the active organization is determined via a cookie. It sounds like you might have this set up as a route parameter.

    If that's the case, ensure you attempt to query the workspace through the authenticated user. If a workspace is not found, you'll want to either:

    1. Throw an unauthorized exception to prevent the user from working with the workspace. You can then offer some button to take them back to safety.

    2. Or, find a valid workspace for the user and redirect them to it. You'll also want to notify them that they did not have access to the previously requested workspace.

    The first option there probably being the best as it will ensure the user knows what happened. You don't want to continue onward as the URL contains an invalid id for the user, which could accidentally be used to load something or on subsequent requests sent from whatever page is rendered as a result.

    Hopefully that's at least a little on point and helps a little. If not, please feel free to share your query/flow, or a representation of it, and I can try and see if anything stands out.

  • Replied to Hii, there is a bug, clicking on 'next lesson' redirects to ...

    Hi usources! Apologies about that, and thanks so much for letting me know! Should be fixed up now! 😊

  • Replied to Yeah. In my app I wasn't using persisting layouts and was just...

    Awesome, happy to hear you were able to get it working! Anytime!

    I have a few lessons on pagination (linked below), but nothing to that extent at present, unfortunately. I debated on whether to include a table with pagination, sorting, and filtering in this series but opted against it due to the series already being lengthy. I'm thinking I might do that as an aside series though.

  • Replied to How do you handle re-rendering? If you put toast-manager into...

    Hi Pavlo! In Inertia, the built-in layout layer is positioned above the page component. This means that the instance of the layout component persists across page changes, as long as the page is linked to using Inertia’s Link component.

    Since we're using the built-in layout layer in Inertia & our ToastManager lives within our AppLayout and AuthLayout, we're achieving exactly that. The only exception would be if we switch between those layouts - in which case you could use nested layouts to resolve that if needed.

  • Replied to Thank you very much for this detailed response; it will help...

    My pleasure, Nicolas!! 😊

  • Replied to Thanks! I simplified it by adding an is_default column to the...

    Anytime! Awesome, that's a perfect solution!! 😊

  • Replied to Hi, thank you very much for this content, it's really very well...

    Hi Nicolas! Thank you so much for your kind words!

    Funny enough, we'll be following this series up by adding an API to it. It'll be a quicker series than this one, and we won't be adding all the features via API, but we'll structure it as though we were.

    I personally would stick to actions, services would be a like alternative, because it provides a single location for our operations to live. It allows us to minimize operational duplication between the web & API layers, which in turn simplifies any refactoring needs down the road.

    As for the routes, with this application in mind, yes I would duplicate those so that we have a route definition specific to the web & API layer. For the web layer, it is unlikely to ever need to do versioning as everything can be updated at the same time. The API layer, however, would likely need versioning so we don't randomly break the clients using the API with changes, with the assumption they won't be updated along with the API itself. Additionally, the middleware between these two are likely to be different and our API layer may need to be more complex with filtering and the like. Because of those things, I would also have separate controllers for the API layer as well.

    However, if none of the above are issues for an API you're building then you don't need to do that. You can instead use a single route definition for both with a single controller that uses content negotiation to determine whether the client needs an API or a Web/Inertia response. You can always separate the routes/controllers between the web and API down the road. The approach there will vary depending on the project your building and its requirements.

    Hope this helps!

  • Published lesson Setting Up Secondary TailwindCSS Config & CSS File for our Landing Page

  • Published lesson Restricting Login Attempts with Rate Limiting

  • Published lesson Clearing Login Attempt Rate Limits on Password Reset

  • Replied to Is there a simple way to query on a relation's relation? For...

    Yeah, if I'm following correctly, you could probably achieve that with nested whereHas calls. This builds out WHERE EXISTS SQL clauses to query based on a relationship's existence.

    const user = auth.use('web').user!
    const organization = await Organization
      .query()
      .whereHas('users', (users) => users
        .where('users.id', user.id)
        .whereHas('setting', (setting => setting
          .where('name', 'default organization')
          .whereRaw('settings_id = organizations.id') // use db names in whereRaw
        )
      )
      .first()
    Copied!

    If I'm thinking about this correctly, this would query the first organization where the id matches that of the authenticated user's user_settings.settings_id and the matched user_settings.name is also default organization.

    Alternatively, you could query for the user_settings id first, then use that to query the organization.

    let organization: Organization
    const user = auth.use('web').user!
    const defaultOrganization = await user
      .related('setting')
      .query()
      .where('name', 'default organization')
      .select('settingsId')
      .first()
    
    if (defaultOrganization) {
      organization = await user
        .related('organizations')
        .query()
        .where('id', defaultOrganization.settingsId)
        .first()
    }
    Copied!

  • Replied to Hi Tom, I usually work with SQLite or Postgres, but I was wondering...

    Hi n2zb! Yeah, absolutely! I haven't done it myself, but it is definitely possible. AdonisJS is still using NodeJS at the end of the day, so you could use Firebase's NodeJS SDK so long as it works with NodeJS v20 or later & supports ESM.

    Now - it won't work out of the box with a few AdonisJS packages that rely on Lucid, like:

    • Auth - though it can be configured with a custom guard

    • Bouncer

    • Limiter - though it'd work with Redis

    • Likely others

  • Replied to Hi! I noticed that in your tutorial, you're able to directly...

    Hi cortesbr! You shouldn't need to call toObject() to access the model's properties; you should be able to directly reference them via movie.id, for example. Please make sure you're using at least NodeJS v20. If you are using NodeJS v20 or later, please feel free to share your repository and I can see if anything stands out.

  • Replied to Is this really the only clean way to tell Edge to pass this ...

    The second argument of the route method is where we'd add route parameters! So, if you need a route for:

    router.post('/movies/:id/activate', [MoviesController, 'activate']).as('movies.activate')
    Copied!

    You could use the route method in EdgeJS to generate it like so:

    <a href="{{ route('movies.activate', { id: 1 }) }}">
      Activate
    </a>
    Copied!

    The third argument is then additional config options, which includes qs to add to the URLs query string. So, there are other options you can include beyond qs to the third argument. Alternatives to this would include, as you've discovered, hard coding the query string outside the route method. You could also use the Route Builder, I believe you'd need to add this as a global to EdgeJS as I don't think it is included out of the box. However, the Route Builder would be AdonisJS solution to your second example!

    What I normally do, though, is wrap the route helper in my own service to make things super easy to read! For example, a usage of my form service would be:

    <form method="POST" action="{{ form.delete('redis.flush') }}">
    </form>
    Copied!

    You could also create EdgeJS components for this as well, which I've done in the past so you could do:

    @form.delete({ action: route('redis.flush') })
    @end
    Copied!
  • Replied to From a UI perspective, if someone has a lot of organizations...

    Yeah, that's a great question, and the approach you'd take will vary depending on either expectations or production data as you don't want to solve issues that don't/won't exist.

    In our application, my expectation is that no one realistically would use more than 5 organizations. Since it isn't something I foresee being an issue within our application, if anything, just setting an overflow on the dropdown group listing the user's organizations would be the solution.

    .organization-select div[role="group"] {
      max-height: calc(100vh - 230px);
      overflow-y: auto;
    }
    Copied!

    We could then do occasional audits on production data to check for use-cases where our expectations may have been wrong and our game plan needs altered.

    If, however, production data shows or our expectations were that users would have 10+ organizations we'd want to take a different approach. It wouldn't be a good experience to list that many organizations within the dropdown; 5 would be ideal, 10 would probably be the max we'd want to show.

    So, what we could do is add a timestamp column to the organization_users pivot table to store when the user last used the organization, maybe called last_used_at. Within our SetActiveOrganization action, we'd be sure to update this timestamp for the user's new active organization.

    Then, within our OrganizationMiddleware where we are getting the user's organizations to list in the dropdown, we'd add a descending order for the last_used_at column and limit the results to 5. Resulting in the user's last 5 used organizations being shown in their dropdown. Then, we'd add one more option to their dropdown called "View All Organizations." This could then link to a new paginated page listing all the user's organizations, again defaulting this list to a descending sort by the last_used_at.

    Alternatively, you could just let the user set their own preferred ordering like we did with our difficulties, access levels, etc.

  • Published lesson Rolling Our Own Authorization Access Controls

  • Published lesson Applying Our Server-Side Authorization Checks

  • Published lesson Applying Our Authorization UI Checks

  • Replied to Hello Tom, what's the difference between imports using subpath...

    Hi n2zb! Imports using the @ character, like @adonisjs/core are actual NPM packages/dependencies installed within our application. The @ character in NPM designates a scoped package. Scoped packages provide a way for the package maintainer to keep all their packages grouped together within the NPM registry. Additionally, it also provides some verification to installers as once a scope is claimed, one the user/organization that claimed the scope may use it.

    For example, since the AdonisJS Core Team claimed @adonisjs only they can register packages using the @adonisjs scope on NPM.

    The # imports, on the other hand, are an actual NodeJS feature called Subpath Imports. These are path aliases pointing to actual files within your project. Let's say we're importing a model:

    import User from '../models/user.js'
    Copied!

    Without using a path alias, we need to traverse to the directory our models are within, for example using ../ to go back a directory. We also need to use the .js extension, required by ES Modules as they resolved and cached as URLs.

    However, if we define a Subpath Import for our models:

    {
      "imports": {
        "#models/*": "./app/models/*.js"
      }
    }
    Copied!

    NodeJS will use this as a reference to fill in the blanks, allowing us to drop the relative paths to the files we're after and simplifying our imports.

    import User from '#models/user'
    Copied!

    Hope this helps clear things up!!

  • Replied to Hi Tom, why do you define an overlay on redis with the CacheService...

    Hi n2zb! We started our CacheService without using Redis, but rather an in-memory key/value store. We kept using the CacheService even with Redis for a couple reasons.

    1. I felt it would be easier to understand and follow by refactoring our preexisting service to use Redis rather than directly replacing the service with Redis. That way the before/after is all in one file.

    2. To keep the JSON parse & stringify calls abstracted to the service's get & set methods.

    3. When we have things like this that may be swapped out at some point, I like to keep them abstracted in my code to simplify any updates down the road. For example, if this lesson were released today we would've used Bentocache rather than the AdonisJS Redis package directly as Bentocache is specially attuned for caching with Redis - and will be used for the official AdonisJS Cache package coming soon.

    So yes, you're pretty spot on! 😊

  • Replied to Tailwindcss 4.0 is released and it does not work out of the ...

    Yes, thank you very much cinqi! When v4 launched I added a note to the top of this lesson, but failed to realize the plugin Shadcn-Vue uses likely isn't up-to-date with that yet. I've updated my note to recommend sticking to v3 for now when following this series.

    I also have a patch to the video being rendered as we speak, which I should be able to get updated tomorrow.

  • Replied to I've been working with Laravel since version 7 but Adonis is...

    I'm really happy to hear you're enjoying AdonisJS, mrs3rver!! 😃

    Thank you very much, I greatly appreciate that!!

  • Upvoted comment very good

  • Replied to very good

    Thank you, mrs3rver!!

  • Upvoted comment Thanks!

  • Replied to Thanks!

    Anytime, Aaron!!

  • Replied to I think the button color is coming from inertia_layout.edge....

    Ah okay! Do you still have the TailwindCSS CDN & project presets within your inertia_layout.edge file? That could be clashing with the config we set up in lesson 1.4. I think we also clear those defaults out in that lesson as well.

    If you'd like some additional eyes on it, please feel free to share a link to the repo! 😊

  • Published lesson Canceling an Organization Invite

  • Published lesson Removing an Organization User

  • Published lesson Refreshing Partial Page Data

  • Replied to I have &lt;Alert v-if="exceptions.E_INVALID_CREDENTIALS" variant...

    That's odd - do the tailwind classes work okay? Maybe your colors got adjusted, if you followed the Shadcn-Vue setup one-to-one, the colors used by Shadcn-Vue should be defined inside our app.css file. Do your colors match mine here? Otherwise, I think the other option in the Shadcn-Vue setup puts them in the tailwind.config.js file.

  • Replied to When you say we might want to add a referrer policy to this ...

    Hi Arron!

    Great question, I now wish I would've injected this into the lesson; I think I'll make a note to do that.

    The Referrer-Policy mentioned is a response header. So, in the controller rendering this page you'd add it using the HttpContext's response, like below. This is a step recommended by OWASP.

    async reset({ params, inertia, response }: HttpContext) {
      const { isValid, user } = await VerifyPasswordResetToken.handle({ 
        encryptedHash: params.hash 
      })
    
      response.header('Referrer-Policy', 'no-referrer') // 👈
    
      return inertia.render('auth/forgot_password/reset', {
        hash: params.hash,
        email: user?.email,
        isValid,
      })
    }
    Copied!
  • Completed lesson Forgot Password & Password Reset

  • Upvoted comment Thank you. 👍

  • Replied to Thank you. 👍

    Anytime!! 😊

  • Upvoted discussion alpine js and edge js

  • Replied to discussion alpine js and edge js

    This was answered on Discord, but I'll go ahead and answer here in case others come across it wondering the same.

    EdgeJS renders HTML markup on your AdonisJS server while AlpineJS mutates the markup in the browser. Once the markup reaches the browser, EdgeJS has already done its job and is out of the picture, so what is being asked isn't possible.

    Instead you'll need to use a client-side solution, like Tuyau, if you want to generate routes. Or, you can just hard code the route and use AlpineJS to populate the id that way.

    <form :action="`/reviews/update/${selectedReview}`">
    </form>
    Copied!
  • Published lesson Listing Current Organization Members

  • Published lesson Sending an Invitation to Join Our Organization

  • Published lesson Accepting an Organization Invitation

  • Published lesson Adding the Organization Invite User Interface

  • Replied to Hello,With useTemplateRef, I’m encountering type errors with...

    Hi gribbl!!

    So, I looked into this a little bit, and it seems like everything TypeScript related for these components that are auto-imported via unplugin-vue-components begins working if you add the generated components.d.ts file into the /intertia/tsconfig.json include array, like below.

    {
      "extends": "@adonisjs/tsconfig/tsconfig.client.json",
      "compilerOptions": {
        "baseUrl": ".",
        "jsx": "preserve",
        "module": "ESNext",
        "jsxImportSource": "vue",
        "paths": {
          "~/*": ["./*"],
        }
      },
      "include": ["./**/*.ts", "./**/*.vue", "../components.d.ts"]
    }
    Copied!

    Here's the result:

    I'm going to go tack a note about this into lesson 1.4, where we setup unplugin-vue-components!

  • Replied to The import for the UserDto works fine in my inertia.js file,...

    Hi Aaron! Maybe try giving VSCode a reload by hitting cmd + shift + p then select for "Developer: Reload Window"

    There's a bug in VSCode that creeps up every once in a while that causes Vue files to lose intellisense & auto imports. Even You (Vue's creator) posted about it a few months ago; you might be getting caught in that bug here. Reloading usually resolves it when I run into it.

  • Replied to Awesome thanks! Will try it out :)

    Anytime!! 😊

  • Replied to Is there an existing equivalent of setters? For example, I'd...

    Hi Rafal! Yeah, both are possible! Just like getters, setters can't be asynchronous. However, methods can be asynchronous.

    export default class User extends BaseModel {
      // ...
    
      @column()
      declare active: boolean
    
      set isActive(value: boolean) {
        this.active = value
      }
    
      deactivate() {
        this.active = false
      }
    }
    
    const user = new User()
    
    user.isActive = false
    user.deactivate()
    Copied!
  • Replied to If we want to define single or multi column indexes for our ...

    Yeah, you can do that via a migration! There is an index and dropIndex method for that. The KnexJS documentation has more info on the options these accept, which will vary depending on your database driver.

    import { BaseSchema } from '@adonisjs/lucid/schema'
    
    export default class extends BaseSchema {
      protected tableName = 'index_examples'
    
      async up() {
        this.schema.alterTable(this.tableName, (table) => {
          // single
          table.index('column_one')
    
          // multi-column
          table.index(['column_two', 'column_three'])
    
          // custom named
          table.index(['column_four'], 'custom_index_name', { /* additional opts */ })
        })
      }
    
      async down() {
        this.schema.alterTable(this.tableName, (table) => {
          // single
          table.dropIndex('column_one')
    
          // multi-column
          table.dropIndex(['column_two', 'column_three'])
    
          // custom named
          table.dropIndex(['column_four'], 'custom_index_name')
        })
      }
    }
    Copied!
  • Replied to @tomgobich I followed your instructions to have the unplugin...

    Hi, csaba-kiss!! You can try registering radix-vue's unplugin-vue-components plugin to see if that helps. The caveat here is you'll likely need to use the original name defined in radix-vue, rather than renamed components from shadcn-vue.

    For example, to auto-import, you'd probably need to use the name "SplitterPanel" rather than shadcn-vue's "ResizablePanel."

    I noticed a similar issue when rebuilding the Adocasts CMS and opted to import the components directly instead.

    import Components from 'unplugin-vue-components/vite'
    import RadixVueResolver from 'radix-vue/resolver'
    
    export default defineConfig({
      plugins: [
        vue(),
        Components({
          dts: true,
          resolvers: [
            RadixVueResolver()
    
            // RadixVueResolver({
            //   prefix: '' // use the prefix option to add Prefix to the imported components
            // })
          ],
        }),
      ],
    })
    Copied!
  • Replied to Thank you for this great tutorial. I'm a beginner but I managed...

    That's awesome to hear marion-huet!!! Thank you for watching! I hope you enjoyed and also found everything easy to understand and follow.

  • Replied to Thank you for taking the time to explain this to me, it's much...

    Awesome, I'm glad to hear that, gribbl!! Anytime!!

  • Anniversary Thanks for being an Acocasts member for 4 years

  • Published lesson Allowing Users to Safely Update Their Account Email

  • Published lesson Alerting Users When Their Account Email Is Changed

  • Published lesson Account Deletion & Cleaning Dangling Organizations

  • Published lesson Updating & Deleting an Organization

  • Replied to Thanks for that explanation. I got an idea in 5.3 of how large...

    Anytime!! Yeah, they grow pretty easily & quickly!! lol 😅

  • Replied to What's the benefit of using actions instead of keeping that ...

    As a whole, by using actions you're:

    • Giving the operation a name & scope, which improves the ability to scan your controllers.

    • Gives us the ability to easily reuse the code if needed. It is considered bad practice to call one controller method from another, as controllers are meant to handle routes.

    • On top of reusability, if we need to update an operation later on, we only have to perform that update in one spot rather than each spot it is performed in the controllers.

    • As you said, it gives us a separation of concerns. If we're performing an action, we know we can find it within our actions folder, under the resource/section it is for.

    Note that actions are very similar to services. To compare, you can think of each subfolder within actions as a service class and each file within the subfolder as a service class method. The main difference here though is actions allow us to easily define steps within the actions whereas that'd need to be additional methods inside a service, which can make things a little messy.

    A good example, though the code for this is from a future lesson releasing soon, is the account deletion controller & action. The controller is easy to scan. The operations have a designated location & scope and can make use of subactions. We're reusing our webLogout method, meaning we can easily update just the webLogout method if we ever need to adjust how we handle logouts. For example, if you're tracking user sessions, you might need to mark that session record in the database as logged out in addition to actually logging out the user.

    @inject()
    async destroy({ request, response, session, auth }: HttpContext, webLogout: WebLogout) {
      const user = auth.use('web').user!
      const validator = vine.compile(
        vine.object({
          email: vine.string().in([user.email]),
        })
      )
    
      await request.validateUsing(validator)
    
      await DestroyUserAccount.handle({ user })
    
      await webLogout.handle()
    
      session.flash('success', 'Your account has been deleted')
    
      return response.redirect().toRoute('register.show')
    }
    Copied!

    I've worked with code, and written my fair share, where the controller method gets unwieldy large because all the operations are done directly in the controller. It might seem small at first, but requirements change, time gets crunched, and things slowly grow over time.

    Hope this helps!

  • Replied to I don't know why, if it's just me or Windows, but I'm getting...

    Are you using NPM directly within Powershell? Powershell reserved the @ character for their splatting feature. Which means you'll either need to escape that character or wrap it in strings, as you found.

    We're using DTOs because getting an accurate serialized type for Lucid models isn't currently possible. If you use the model as a type directly in Inertia, the type will include things you don't have access to in your frontend, like .merge({}), .save(), .related('organizations'), etc.

    If you use just the model's attributes, that won't include relationships, extras, and also won't take into account serialization alterations on the model like serializeAs.

    So, currently, it is due to technical constraints in Lucid and would require some hefty refactors to remedy. Yeah, if you were trying to do end-to-end types with an API you'd run into the same constraint.

  • Replied to I hadn’t seen the schedule page. Nice ! The idea of an API is...

    Yeah, those seem to be the areas most struggle with on APIs 😊. Using the PlotMyCourse application also gives us a cool opportunity to cover authentication with a non-user entity.

  • Replied to No problemo.That's strange. I have the exact same code as you...

    I'm so sorry, my head was in the wrong spot! You're absolutely right, the /storage gets added to the saved value in the database so the unlink should be:

    await unlink(app.makePath('.', movie.posterUrl))
    Copied!

    Terribly sorry for the confusion there! I'll make a note to correct this lesson.

  • Replied to Hello. I believe that with recent versions of Vue, it is possible...

    Hi gribbl! Thank you for sharing! Yeah, I had missed a number of Vue 3.4's enhancements. I apologize we didn't use any of them in this series. As for 3.5's enhancements, this codebase is still on 3.4 - I don't want to update any packages mid-series until we get to the end where we'll focus on Inertia v2's changes.

  • Replied to Not a problem brother, appreciate the work you're putting into...

    Thank you, yunus! I very much agree in regards to using Inertia over meta frameworks when you need the full stack! 😊

    As for deployment, please see my comment here. The TLDR is it's coming, but not something I do myself all that often. So I just need to dedicate some time to do proper R&D to do some lessons justice. 😁

  • Replied to Hi! Just wanted to share that a new update for @adonisjs/inertia...

    Yes, thank you for sharing!! Once completed, we'll be adding a new module to the end of this series covering what's new in Inertia v2 and the AdonisJS adapter and I'll circle back and add/edit some lessons to include notes for outdated things like this.

  • Published lesson New Unique & Exist Validation Overloads in AdonisJS 6

  • Replied to It would be great if the right hand sidebar showed completed...

    Yes, it absolutely would, thank you very much kite!! I have no clue how I overlooked that. I'll try and get this added tomorrow after work!

  • Replied to Im following along using SolidJS, but got stuck at the auto ...

    Hi yunus! Apologies for the delayed response! I'm not familiar with the SolidJS ecosystem in the slightest, so I unfortunately do not know. You might have luck using unplugin-auto-import, that is what unplugin-vue-components utilizes, but I'm not certain whether it'll work nor how it'd need to be configured.

  • Replied to The Vue ecosystem is fantastic. I’ve also set up Prettier with...

    Yeah, I absolutely love Vue & it's ecosystem!! Thank you for sharing these plugins' gribbl!! 😊

  • Replied to Thank you for this course. I’ve learned so much, but there’s...

    Thank you for watching, gribbl! I'm ecstatic to hear you learned a lot from it and are loving AdonisJS!!

    I discovered AdonisJS back in the early AdonisJS 4 days. I was previously working with Laravel, but I work primarily with JavaScript and found AdonisJS searching for a JavaScript alternative to Laravel. 😊

    I've had a lot of requests for some API content, so we'll be doing a series on adding an API to the PlotMyCourse application we're currently making in the Building with AdonisJS & Inertia series. On top of that, we'll also be doing a todo series, showing how to make a todo app in a bunch of different ways with AdonisJS. I have some other ideas on our schedule page, but those are more open to changes before we get to them. I like the idea of an e-commerce site, I'll add that to the ideas, thank you for the suggestion!! 😊

    Happy New Year to you as well!! 🎉

  • Replied to Hello,I'm going to bother you again. 😆 I think there’s still...

    Yep - you're absolutely right, sorry about that! Tacking one more property into the object specifically to uniquely identify the row would fix it up and Math.random() is usually what I reach for as well for those use cases.

    I like to use the below because the _ prefix ensures it can also be used in the id attribute if needed.

    const uniqueId = '_' + Math.random().toString(36).substr(2, 9)
    Copied!
  • Replied to I don’t have a public directory, only storage at the root of...

    Sorry for the delayed response! Are you able to share the repo url? Neither should be the case, and I can't immediately think of anything that would cause either of those.

  • Published lesson Creating the Settings Shell

  • Published lesson User Profile Settings

  • Replied to Hello,When I try to access uploaded file, for example /storage...

    Hi Gribbl!

    If you're able to directly access the file via the URL without going through a route definition, that leads me to believe the file is actually within your public directory. Files within this directory are made accessible by the AdonisJS static file server.

    That would also explain the "no such file" error on the attempted deletion, as app.makePath('storage') is going to be looking in the ~/storage directory directly off your application root, not within the public directory.

    Can't say for certain though - just making an educated guess based on the info provided.

    Hope this helps!

  • Upvoted discussion Will deployment be covered?

  • Replied to discussion Will deployment be covered?

    Hi Mohamed!

    Good question about production deployment! While I don't currently plan on including it in the "Building with AdonisJS & Inertia" series, it's absolutely on my radar and something I plan to cover in the future.

    It's not my strongest area, and I don't regularly do it, so to sufficiently cover security concerns I need to invest some time into learning enough to do it justice. I currently use Cleavr, which does all that for me. After investing some R&D time, I may decide to tack it onto the end of the Inertia series or cover deployment in a series of its own.

  • Replied to discussion How to classify Schema nama in adonisjs 6 migration file?

    Hi sivaclikry! As of right now, there is no way to generate models and migrations from an existing database. So, you'll want to manually define the models and migrations you'll need.

    I have a package that is a work in progress, you're welcome to try it. It'll generate models for you, but it doesn't currently do migrations. Right now, it'll work best with MySQL & PostgreSQL.

    To install, you can run:

    npm add @adocasts.com/generate-models
    Copied!

    Ensure your db connection is defined and working in your .env , then generate with:

    node ace generate:models
    Copied!

  • Published lesson 3 Easy Ways to Split Route Definitions into Multiple Files in AdonisJS 6

  • Published blog post Testing Cloudflare R2 for Video Storage

  • Replied to Please Share me how to work with Transmit inertia vue.

    Hi laithong! I haven't yet worked with Trasmit, but I don't believe using Inertia should change its implementation any from what is covered in the docs.

    The server-side installation would be the same as covered in the documentation; installing the packages, configuring your Redis connection, registering the routes, and defining the channels.

    Then, you'd define the client initialization inside a file within your Inertia directory, somewhere like ~/inertia/lib/transmit.ts, and import it where needed. I imagine the client will only work client-side in the browser. So, you may want to ensure whatever imports your transmit file is running only on the client and not using SSR. I'm not certain on that though.

  • Published lesson Patching Tag Changes for our Modules & Lessons

  • Published lesson Storing Module Order Changes from Vue Draggable

  • Published lesson Storing Lesson Order Changes & Handling Cross-Module Drag & Drops

  • Replied to You can also do it like this in FormDialog.vuedefineProps&lt...

    Wow - thanks a ton for sharing this, Jeffrey! I completely missed Vue adding in the defineModel macro, that's a huge improvement!!

  • Replied to discussion Selective mail transport driver installation in adonisjs

    Similar to Lucid, when you configure the AdonisJS Mail package, it'll ask you specifically which drivers you intend to use and it will only install those applicable to your selections.

  • Replied to I believe there has been an update. Now, it is possible to use...

    Hi gribbl! Yep, absolutely!! It is officially documented within Lucid's documentation!! 🥳 A great new addition!

  • Replied to export default class User extends compose(BaseModel, AuthFinder...

    Hi mojtaba-rajabi! I wouldn't recommend trying to redirect the user from inside a method on the model.

    1. There's a high probability the redirect would get forgotten about should you ever need to debug

    2. You'd still need to terminate the request inside the controller method/route handler.

    If this is for AdonisJS 6, what I would recommend instead is to let the framework do its thing. The findForAuth method is called by the verifyCredentials method. This method purposefully hashes the provided password, regardless if a user is found or not, so that there isn't a timing difference between the two. Reducing the ability for a bad actor to scrape your login form for credentials &/or valid emails using a timing attack.

    This verifyCredentials method then throws an exception if the login is wrong, which you can capture and display on the frontend. This exception is available via the errorsBag flash message. That covers your if (!user) check.

    Then, I would let verifyCredentials return back the user if one is found, regardless of the isActive flag and instead check that isActive flag after verifying the credentials. That way:

    • You can keep timing attack protections AdonisJS set for you in place

    • You know the user is correct, since they know their credentials, so you can guide them (if applicable) on how to activate their account.

    • You don't have any sneaky code doing things in places you might not expect a year or two down the road.

    You can then easily pass along whatever message you'd like via Inertia using flash messaging.

    public async login({ request, response, session, auth }: HttpContex) {
      const { uid, password } = await request.validateUsing(loginValidator)
      const user = await User.verifyCredentials(uid, password)
    
      if (!user.isActive) {
        session.flash('error', 'Please activate your account')
        return response.redirect().back()
      }
    
      await auth.use('web').login(user)
    
      return response.redirect().toRoute('dashboard')
    }
    Copied!

    Hope this helps!

  • Published lesson Creating & Listing Sortable Course Lessons

  • Published lesson Editing & Deleting Course Lessons

  • Published lesson Adding A Publish Date & Time Input

  • Upvoted comment No worries, Thanks a lot Tom.

  • Replied to No worries, Thanks a lot Tom.

    Anytime, jabir!

  • Replied to Hi,First, thank you for your video ! That's very clear. around...

    Hi Pierrick, thank you for watching!

    You can find this within lesson 1.1 - "What We'll Need Before We Begin." At the 3:10 mark, we created the database "adonis6" for use in this series, I apologize; it seems I misspoke and said table in this lesson when I meant database.

  • Replied to Hi Tomgobich, thanks for your advice! It seems like verification...

    Anytime! Yep, definitely some movement there!! 😊

  • Published lesson Model Query Builder Macros in AdonisJS 6

  • Replied to Hi Tom, Thanks for the detailed response! 1. I was specifically...

    Anytime!!

    I'm fairly certain the browser displays the popup before the response of the request is actually received, at least that is the behavior I've noticed in the past with Firefox (the browser I use).

    I, unfortunately, don't think or know of a way where is a way to enable/disable depending on success. I tested on Laravel Forge's site and still got the popup with invalid credentials (screenshot below). I can't say for certain it is impossible to do, but to the best of my knowledge I'm not aware of a way. Sorry, I know I haven't been much of a help here.

  • Replied to In AdonisJS v6, when using the access token authentication guard...

    So long as the underlying data columns remain the same and the changes you're looking to do adhere to the type requirements set by the @adonisjs/auth package I don't think there would be any issues with reusing the table for other tokens. I don't foresee an enum column type causing issues either, so long as you include auth_token as one of the values in the enum.

    If you give it a try, let me know how it goes! 😊

  • Replied to Hi Tom, 1) Is it possible to prevent the browser from displaying...

    Hi jabir!

    1. This is a native browser behavior when forms are submitted with a password field. So, I suppose one way would be to not use the type="password" on the input, but I wouldn't recommend that as prying eyes could easily grab someones credentials. There's a long discussion on StackOverflow of some attributes that may do the trick, though I'm not sure which are working as of today since browsers tend to change behaviors like this over time.

    2. Is this with forward navigation or using the browser's back button? If it is with the browser's back button, I don't think there is much you can do as that restores the saved state of the page in the browser's navigation history. The behavior you see there is going to match any multi-page application. If it is forward navigation, then that shouldn't be happening and there is likely a state update issue somewhere in your code. Happy to help dig in if it is a forward navigation issue and you're able to share code.

  • Published lesson Querying & Listing Sortable Course Modules

  • Published lesson Creating, Editing, & Deleting Course Modules

  • Published lesson Deleting Courses

  • Published lesson Showing A Course's Details

  • Published lesson The Tag Selector

  • Replied to Thanks to your reply, I understand why it wasn't working. In...

    Anytime! Awesome, I'm happy to hear you were able to track it down and get it all fixed up! 😊 Another alternative, if applicable to your use-case, is to reflash any messages. This will push the flash messages forward with a secondary redirection.

    Flash → redirect home → reflash & redirect family.show

    export default class UserHasFamilyMiddleware {
      async handle({ auth, response, session }: HttpContext, next: NextFn) {
        if (auth.user && auth.user.hasNoFamily()) {
          session.reflash()
          return response.redirect().toRoute('family.show')
        }
        return next()
      }
    }
    Copied!
  • Replied to Thank you so much for making this feature available so quickly...

    My pleasure, thank you for the suggestion! 😊

  • Replied to discussion Is it possible to dowload courses videos to watch it offline?

    Hi noctisy!

    This is something I've been meaning to add, but kept slipping off my radar.

    I'll preface by saying I'm not going to add download support for videos we only have hosted on YouTube, which are typically our older videos. Though that is technically possible, I'm sure that'd be against some ToS of theirs and I do plan on moving these videos over to our storage service anyways.

    As for our newer videos, which I think fully encompasses all of our AdonisJS 6 lessons, I've just deployed out an update to add a download button for Adocasts Plus subscribers! You can find it just below the video. Again, this is brand new, so if you run into any issues please do let me know!

    As a warning, a lot of our videos are uploaded as WEBM, and that is how they'll download as well. So, you'll need to use a video player that supports WEBM videos.

  • Published lesson Querying & Listing An Organization's Courses

  • Published lesson Creating A New Course

  • Published lesson Editing & Updating Courses

  • Replied to thanks . i am using adonis 6 . '@ioc:Adonis/Core/Application'...

    Anytime! Ah - then it'll be import app from '@adonisjs/core/services/app'

    import app from '@adonisjs/core/services/app'
    
    class SocketioService {
      inProduction() {
        return app.inProduction
      }
    }
    Copied!
  • Replied to Hello, Sorry if you covered this point in another lesson, but...

    Hi davidtazy!

    Yeah, the toast system should be fully working by the end of this lesson. And, in fact, In lesson 5.2 Logging Out Users, we add in a session.flash('success' Welcome to PlotMyCourse') after successfully registering the user.

    Are you redirecting the user after registering them? Flash messages are made available for the next request and won't be applied to the current, so the redirect does serve a purpose here.

    Here is the final RegisterController's Store method.

    @inject()
    async store({ request, response, session }: HttpContext, webRegister: WebRegister) {
      const data = await request.validateUsing(registerValidator)
    
      // register the user
      await webRegister.handle({ data })
    
      session.flash('success', 'Welcome to PlotMyCourse')
    
      return response.redirect().toRoute('organizations.create')
    }
    Copied!

    If you'd like & are able, feel free to share a link to the repo and I can see if anything stands out.

    Also, that's awesome to hear! I hope you enjoy AdonisJS and thank you for joining Adocasts Plus!! 😁

  • Completed lesson Logging Out Users

  • Replied to Thank you a lot for the clarification Tom! It was a bit confusing...

    Awesome & anytime! I'm happy to hear things are clicking for you. If there is anything you feel would help make things clearer for the first go around, I'm all ears!! 😊

  • Replied to Thank you for this quick response. So if I understand correctly...

    Anytime! Yes, sorry, we'll move our home page into our group protected by the auth middleware in the next lesson (6.0). So, that warning specifically on the home page will go away in the next lesson.

  • Replied to thank for tutorial.how can add app instance when new() a singleton...

    So long as you're importing and using this service in a context where the application has already been booted, then you should be able to just import and use app as it is a singleton.

    import Application from '@ioc:Adonis/Core/Application'
    
    class SocketioService {
      inProduction() {
        return Application.inProduction
      }
    }
    Copied!
  • Replied to Hello Tom, First of all, thank you for all this amazing work...

    For the authenticated user to be populated you must inform AdonisJS to check for it. This saves the roundtrip to populate the user in cases where it isn't needed.

    To populate the user, you have two options

    1. authenticate - Requires an authenticated user. If an authenticated user is not found, an exception is thrown.

    2. check - Will check to see if a user is authenticated, and populate that user if so. The request goes on as usual if an authenticated user is not found.

    In terms of middleware, you have three options

    1. auth - Will call authenticate and redirect the user to the login page by default if an authenticated user is not found.

    2. guest - Will call check and redirect the user, I believe to the home page, by default if an authenticated user is found.

    3. silent_auth - Will call check and progress with the request as usual.

    So, you're most likely getting "the page isn't redirecting properly" because your authenticate on the login page is attempting to redirect the user to the login page, resulting in an infinite redirect loop.

    You most likely will want to replace your authenticate on the login page with the guest middleware and that should fix the redirect loop. Then, for internal pages, you can either use authenticate if the user must be authenticated to access the page, check if the user may or may not be authenticated to access the page, or one of the corresponding middleware.

  • Replied to Hello Tom, I'm used to working with Doctrine (ex Symfony users...

    Hi noctisy!

    With Lucid, relationships work with and directly read off of the model. So, if you omit the userId but define the relationship, you'll end up getting an exception.

    Though I don't know the core team's exact reasoning, I'd imagine it is so the relationship can make use of the column decorator's mutations to build out the relationship columns. It could also be for compatibility with hooks as well.

    For example, you might want a different naming convention in the model than in the table.

    export default class Profile {
      @column({ isPrimary: true })
      declare id: number
    
      @column({
        columnName: 'profile_user_id', // column name in db
        serializeAs: null, // omit from serialization (maybe it is confidential)
      })
      declare userId: number
    
      @belongsTo(() => User)
      declare user: BelongsTo<Typeof User>
    }
    Copied!
  • Published lesson Reusable VineJS Exists In Organization Validation

  • Published lesson Sorting Difficulties with Drag & Drop

  • Published lesson Creating A Reusable Sorting Vue Component

  • Published lesson Replicating Behaviors for Access Levels & Statuses

  • Upvoted discussion TailwindCSS plugins installation issue

  • Replied to Hey! How can I send custom errors? I'm validating certFile and...

    Hey Ruslan!

    Yeah, as we'll discuss in lesson 3.6, covering Inertia's limitations, when Inertia sends a request it requires a specific format for the response. Otherwise, you'll get the error you mentioned above.

    Because of this, Inertia doesn't want you to return 422 responses. Rather, they want you to flash the errors to the session and redirect the user back to the page.

    So, if certFile is a form field, you could manually send back an error like this:

    async store({ inertia, request, response, session }: HttpContext) {
      // ...
        
      if (!certFile.isValid) {
        // not sure if certFile.errors is already an array or not
        // certFile type should be string[] for consistency with AdonisJS
        session.flash('errors', { certFile: [certFile.errors] })
        return response.redirect().back()
      }
    
      // ...
    }
    Copied!

    Otherwise, if you just need a general exception-like error, separate from validation, you could do:

    async store({ inertia, request, response, session }: HttpContext) {
      // ...
        
      if (!certFile.isValid) {
        session.flash('errorsBag', { E_UNPROCESSABLE_ENTITY: certFile.errors })
        return response.redirect().back()
      }
    
      // ...
    }
    Copied!

    Hope this helps!

  • Replied to One other question I thought about as well is CSRF token validation...

    No, CSRF is a broad reaching vulnerability across the web and is not specific to inertia nor server-side rendered applications. The goal of these attacks is to hijack a state changing request, causing an authenticated user to perform an action they did not want nor intend to happen.

    You can go more in-depth on CSRF by reading through OWASP's guide.

  • Replied to Awesome thanks for your explanation 😀

    Anytime!! 😊

  • Replied to When it comes to setting up authentication with Adonis but in...

    Yeah, if you're using session authentication using AdonisJS as an API, so long as the two apps share the same domain, the approach would still be the same just without the front-end routes & pages. Depending on your set up you may need to update the session cookie's domain config to allow subdomains.

    In terms of validation, that's up to your discretion. You'll always want to perform server-side validation for data integrity's sake since client-side validation can be easily circumvented. Client-side validation is generally provided to improve user experience since it is a quicker way to provide feedback. When working with reactive client-side frameworks, it is generally considered good practice to validate on both the client & server side.

  • Replied to Hi Tom, could you explain why you are using actions over services...

    Hi Emilien! Yeah, happily! Firstly, just want to point out that using either actions or services are perfectly acceptable options for the project we're building here. They're very similar, the primary difference being services are comprised of one class with many methods whereas with actions the service class is typically a folder and the methods are individual classes inside that folder for each action. So, actions just kind of scoot everything up a level.

    With actions, since a class is dedicated to a single action, this means it is easier to split out sub actions, keeping things organized and easier to read by giving those sub actions names. I can't recall whether we've done this yet at this point in the series, but we most certainly do down the road!

    Those separate classes also aide our ability to inject the HttpContext for specific actions. With services, that becomes a little more complicated and often leads to needing two different service classes (one with & one without the HttpContext injected). Whereas, with actions, we can easily make that determination on a per-action basis, which we do in this module (module 6) of the series.

    Those two points are the primary deciding factors that lead me to use actions over services for this project. Actions may use more files, but that also brings with it more flexibility in how we write, organize, and inject things inside the project.

    That wasn't a foresight thing either, I actually originally started this project with services (shown below) before switching to actions. Once I got to a certain level, I began feeling like actions would be the cleaner and less confusing way to go as opposed to having service methods daisy chaining other service methods to perform an action.

    Hope that answers your question! 😊

  • Published lesson Updating Difficulties

  • Published lesson Confirming & Deleting Difficulties

  • Published lesson Replacing A Course's Deleted Difficulty

  • Replied to Indeed it was this line of code static selfAssignPrimaryKey ...

    Awesome, I'm glad to hear everything is working a-okay now! 😊
    Anytime, happy to help!

  • Replied to Issue created. First time I write an issue, let me know if something...

    Looks good, thank you emilien! I'll try and take a look into it today after work.

  • Replied to Hi, I cannot share my project in a repo because it's a enterprise...

    It might be something related to the UUID implementation. I haven't personally used them in AdonisJS so I'm not overly familiar with everything needed to get them working, but I do believe you need to inform AdonisJS you're self assigning the primary key via the model.

    export default class User extends BaseModel {
      static selfAssignPrimaryKey = true
      // ...
    }
    Copied!

    You can try digging into the node_modules to console log/breakpoint where AdonisJS is attempting to authenticate the user to see if:

    1. Is it finding the userId from the session okay

    2. Is it able to find the user via that id

    Additionally, though this wouldn't cause your issue, both the auth middleware and the auth.check() method calls the auth.authenticate() method, so you only need one or the other.

    • The auth middleware is great when you need the user to be authenticated to access the route. It will throw an error if the user is not authenticated.

    • The auth.check() call is great when the user can be authenticated but doesn't need to be to access the route.

    Hope this helps!

  • Replied to Hello, first thing first thanks for all the work you do for ...

    Hi Alan! Thank you for watching!

    Are you able to share a link to the repository or a reproduction of it? Based on the two snippets, all looks good, but the issue could be in the user model, config, or middleware.

  • Replied to Sorry to bother you again. Some of my model's columns are not...

    Yeah, if you could create an issue that'd be great, thank you! Currently, this package reads the models as plaintext, line-by-line. So, there's room for error based on formatting. I'm hoping to refactor to instead use ts-morph which should fix those oddities up.

  • Upvoted comment Thank you Tom. Eternally grateful.

  • Replied to Thank you Tom. Eternally grateful.

    Thanks for watching, wechuli!! 😊

  • Published lesson The Confirm Delete Dialog & Deleting the Active Organization

  • Published lesson Listing & Creating Difficulties

  • Replied to Is a service just a controller?

    Hi Luiz! Though they look similar and are traditionally both classes, they aren't the same as one another from a logical standpoint as they're in charge of different things.

    Controllers are used to handle routes. AdonisJS will directly instantiate a new instance of a controller and call the appropriate method bound to the requested route.

    Services generally aid controllers and events to help them complete their objectives. This could be something as simple as a helper, like normalizing text, or as complex as the main action of an operation, like storing a new blog post.

    To put it in a real-world example, let's say you go to a dine-in restaurant. You sit at a table and the server comes over to take your order (determining which of the app's routes you need). They take your order (request) to the chef (our controller). The chef then is in charge of completing your order which may consist of:

    1. Making a burger

    2. Fetching fries

    3. Grabbing that pickle slice

    4. etc

    Any one of those little things could come from a service, but the controller (the chef) coordinates and puts it all together to complete the request (order).

    export default class DinerController {
      async order({ request, view }: HttpContext) {
        const data = await request.validateUsing(orderValidator)
    
        const burger = await BurgerService.getCheeseBurger()
        const fries = await FriesService.getMediumFry()
        const pickle = await CondimentService.getPickleSlice()
    
        return view.render('plate', {
          burger,
          fries,
          pickle
        })
      }
    }
    Copied!
  • Published lesson Creating A UseResourceActions Composable

  • Published lesson Editing the Active Organization

  • Replied to No problem, don't apologize. Updated your package and everything...

    Awesome! Glad to hear everything is working for ya now! 😊

  • Replied to Hi Tom, tried using your package to generate dtos, but it gives...

    Hi emilien! Terribly sorry about that! I had missed normalizing the slashes where the model's file name is determined which resulted in the whole file path being used on Windows.

    Should be all fixed up in v0.0.7

  • Published lesson How To Add Social Authentication with AdonisJS Ally & Google

  • Published lesson The Form Dialog Component & Adding Organizations

  • Published lesson Switching Between Organizations

  • Replied to Hello , There's a problem with the subtitles. It doesn't match...

    Hi tigerwolf974! I'm so sorry about that. It looks like I accidentally uploaded the previous lesson's subtitles to this lesson. They should be all fixed up now. Thank you very much for the heads-up!!

  • Replied to thank you a lot …

    Anytime, David!!

  • Upvoted comment Awesome, that was it! Thank you!

  • Replied to Awesome, that was it! Thank you!

    Anytime!! 😊

  • Upvoted discussion App split into modules

  • Replied to discussion App split into modules

    Hi David! Yeah, with AdonisJS you can configure it a ton to your liking, especially the app folder. Things in app are mostly imported and used in your code, rather than bound directly via AdonisJS, so you can configure the folder structure however you see fit.

    For inspiration, you might want to check out Romain Lanz's website. The app folder is structured very similarly to what you're after.

  • Replied to Found the issue, it happened because I forgot to add "await"...

    Hi nonwiz! Happy to hear you were able to get it figured out! 😊

  • Replied to When changing over routes to:router.get('/register', [RegisterController...

    Hi tdturn2!

    First, your import is valid and will work, it just won't work with hot module reloading (HMR). HMR is enabled by default in newer AdonisJS 6 projects. Rather than fully restarting the application when a change is made in development, HMR enables the single updated spot to be updated on the fly.

    AdonisJS uses NodeJS Loader Hooks to perform HMR, and loader hooks require dynamic imports to work and hot-swap modules when updated. If you'd like to read more on this, they walk through the full what & whys in the documentation.

    So rather than importing your controller like this, which will not work with HMR:

    import RegisterController from '#controllers/auth/register_controller'
    
    router.get('/register', [RegisterController, 'show'])
    Copied!

    You can instead dynamically import the controller, which will work with HMR:

    const RegisterController = import('#controllers/auth/register_controller')
    
    router.get('/register', [RegisterController, 'show'])
    Copied!

    If you have your text editor fix lint errors on save, this change will happen automatically when you save your file. You'll see this happen for me in the lesson around the 2:20 mark. If you're using VSCode, you can turn this on by installing the ESLint extension, and then adding the below to your JSON user settings.

    {
      "editor.codeActionsOnSave": {
        "source.fixAll.eslint": "explicit"
      },
    }
    Copied!
  • Upvoted comment Thank tom!

  • Replied to Thank tom!

    Anytime, nonwiz!!

  • Published lesson Setting & Loading the User's Active Organization

  • Published lesson Listing the User's Organizations

  • Replied to Just wondering what's the clean way (if possible) to have a ...

    Hi nonwiz! As we did with the service in this lesson, you can import and use enums directly inside your frontend. It'll then be bundled with the frontend code when you build it.

  • Upvoted comment How can i make it with adonisjs 6?

  • Replied to How can i make it with adonisjs 6?

    The premise would be the same as shown in this lesson, just using the updated code for AdonisJS 6 authentication. You'd want to configure your admin and user guards in the auth config, pointing to the correct model in each guard. Include the AuthFinder mixin in both models. Then, it should essentially be the same login flow as any AdonisJS 6 app, just using the correct guard.

  • Completed lesson Adding the Remember Me Token

  • Published lesson Adding the Remember Me Token

  • Published lesson Forgot Password & Password Reset

  • Replied to By the way thank you for these videos you have great teaching...

    That means a lot, thank you, tibormarias! Also, thank you for watching & being an Adocasts Plus member, it's greatly appreciated! Yeah, if you'd rather not use DTOs, I think that's a perfectly viable alternative approach.

  • Replied to Hey Tom, thanks for fast reply. Actually i think i checked if...

    AdonisJS' automatic 302 to 303 conversion happens via the Inertia Middleware that comes with the package. You'll want to make sure it is configured as server middleware inside the start/kernel.ts file.

    I reckon the 302 issue might be related to your errors not originally populating. If you're able to and need further help digging into it, feel free to share the repo and I can take a look.

  • Upvoted discussion adocasts UI glitch

  • Replied to discussion adocasts UI glitch

    Hey Terry! Sorry to hear you're having issues with the site!

    We used to have a similar issue when we first introduced our mini-player, where the video you exited on would sometimes persist to the next lesson entered. However, this didn't impact titles, descriptions, or body content but rather just the loaded video.

    I'll poke around and see if I can find anything that might cause something like this!

  • Replied to until this part i was amazed how flawlessly AdonisJS works with...

    I agree, it does make things less DRY, but to my knowledge, in other Inertia-supported environments, you would need to define the type specifically for the frontend as well. At least with AdonisJS, we can convert it to the DTO on the server side to ensure what we're passing matches expectations on the frontend.

    It would be awesome if we could directly access a serialized type of the model, but that would take quite the doing!

  • Replied to Hey, my app doesn't provide validation errors right away. On...

    Hey hsn! When you're redirecting, is the response type a 303? Inertia expects redirect statuses to be 303 to work properly, and the AdonisJS adapter should automatically take care of this for you. So, if that isn't happening then something is likely misconfigured in your project.

    As for the attr section, if all items being passed into a page component are registered as props within the page, then there won't be attrs to display as everything will be under props. attrs in Vue captures non-registered prop data. For example:

    <template>
      <Header :user="user" :theme="blue" />
    </template>
    
    ---
    
    <script setup lang="ts">
    // Header.vue
    defineProps<{
      theme: string
    }>()
    <script>
    
    <template>
      <span>Hello</span>
    </template>
    Copied!

    In the above theme is a registered prop, so it'll be under props, but not attrs. Meanwhile, user is not a registered prop, but is being passed into the header. Therefore, it won't be within props, but rather attrs.

    That is the only thing coming to mind as to why the attrs may differ.

    Hope this helps!!

  • Upvoted comment Lesson 7 ✅

  • Upvoted comment Lesson 6✅

  • Upvoted comment Lesson 5 ✅

  • Upvoted comment Lesson four ✅

  • Upvoted comment Lesson 3 done ✅

  • Upvoted comment I have just started my first lesson

  • Replied to Onto my second lesson… it's getting slightly complex but am ...

    Thanks for watching, Jony! Hope all is still going well!! 😁

  • Upvoted discussion Admin panel

  • Replied to discussion Admin panel

    Hi Anjanesh! There isn't one built-in, but there are a couple of third-party options. AdminJS is compatible with AdonisJS, then there is another called Adonis Cockpit that is in pre-release.

  • Replied to Hi Tom, please can you do a section on how to log users in with...

    Hi Carlos! Yes, I've been meaning to record a lesson on that! Perhaps I can get that recorded this upcoming weekend for release soon after.

  • Replied to You can also set your postcss directly in package.json and avoid...

    That's awesome! Thank you for sharing, secondman!! 😁

  • Replied to It's been nearly a year since you published this so maybe you're...

    Thank you, secondman!! Yeah, the folks in the YouTube comments quickly provided feedback when this lesson was released. My pronunciation should be fixed from, I believe, this lesson onward.

  • Replied to I installed IntelliSense as well but the suggestion when typing...

    I believe that panel is called the "quick panel" in Intellisense. You may want to check through your VSCode User JSON settings to see if it may be disabled for you. You can find more info here:

    https://code.visualstudio.com/docs/editor/intellisense#_customizing-intellisense

  • Published lesson Onboarding Newly Registered Users

  • Published lesson Logging In Users & Displaying Exceptions

  • Published lesson User Registration with InertiaJS

  • Published lesson Splitting Our Routes Between Auth & Web

  • Published lesson Logging Out Users

  • Replied to which extension do you use for that autocomplete on vscode? ...

    Hey Chanbroset-Prach! I believe that should just be the default IntelliSense which comes with VSCode.

  • Published lesson Completing Our AppLayout & Navigation Bar

  • Published lesson Creating A Toast Message Manager

  • Upvoted comment cheers!

  • Replied to Thank you Tom for the very detailed answer, it just looks really...

    Anytime! The grass can be a different shade of green depending on the ecosystem you're in, but just because it is different doesn't mean it's a bad thing. The migration approach is a common and popular choice, used in the like of .NET, Laravel, and likely others but those are two I know and have personally used.

    Thank you, Hexacker! The one thing those two (.NET & Laravel) offer that AdonisJS doesn't is model generation, which is why I wanted to make a package to at least offer something in that regard. An approach for model/migration generation has been something planned by the core team, but it is likely still a ways out.

    With model generation in play, the flow would go:
    Define migrations → run migrations → generate models

  • Replied to I just have a question about defining the model. I'm coming ...

    Hey Hexacker!

    In AdonisJS, migrations are what create our tables, foreign keys, indexes, etc. Migrations are great because they give us control to incrementally build or update our database as we need in the order we need. Most of the time, in starting an application, it'll seem tedious. But, as your application ages, they're fantastic for maintaining flexibility.

    For example, if I have a pre-existing database with a histories table that holds two types of histories, view history and progression history. As the app ages, if I decide it'd behoove me to have this one table split into two, I can easily use migrations to create a view_histories table and a progression_histories table. Then, in those same migrations, I can fill them directly with data from the histories table. Then, delete the histories table. These changes would take 3 migrations, which are run one after another at the same time my application with the needed code updates are deploying.

    1. create_view_histories_table
      Creates the view_histories table, then uses the defer method to populate it with pre-existing view histories from the histories table.

    2. create_progression_histories_table
      Creates the progression_histories table, then uses the defer method to populate it with pre-existing progression histories from the histories table.

    3. delete_histories_table
      Drops the no longer needed histories table

    Models then, are strictly used to aide in CRUD operations inside our code. At present, there is no official way to generate migrations nor models. However, I do have a package in dev release to generate models from a database.

    Fun fact, the migration example above is based on a refactor I did on the Adocasts site.

  • Replied to That was a better solution than mine.Thank you for the amazing...

    Thank you for watching!! 😊 That's awesome to hear, thanks Hexacker!! I hope you enjoy your time with AdonisJS!! 😁

  • Replied to Hi Tom - how to do the same setup with react

    Hi Carlos! Though I haven't worked with React since its class days (years ago), unless anything recently has changed, I don't believe React has a concept of global components. So, unfortunately, I don't believe this same global registration will work with React.

  • Replied to discussion Add a webhook endpoint to AdonisJS web app | FIXED

    Glad to hear you were able to get it figured out, Hexacker!

    For anyone else who may come across this, you want to make sure you have CSRF disabled for the route, which is in config/shield.ts:

    /**
     * Configure CSRF protection options. Refer documentation
     * to learn more
     */
    csrf: {
      enabled: env.get('NODE_ENV') !== 'test',
      exceptRoutes: ['/stripe/webhook'], // 👈 add your webhook route(s) here
      enableXsrfCookie: true,
      methods: ['POST', 'PUT', 'PATCH', 'DELETE'],
    },
    Copied!
  • Replied to Are you using a specific icon theme in VS code? My .edge file...

    Hi Jeffrey! Yeah, I'm using the Bearded Icons file icon theme.

  • Replied to Thanks Tom for the tutorial! I am trying to implement upload...

    Thanks for watching, Guy!

    For that, I'd recommend using Drive (FlyDrive). Its goal is to provide a unified API regardless if you're working with the local disk, S3, GCS, etc. We, unfortunately, don't have any lessons on FlyDrive of yet but the documentation is pretty great! With it, the only thing you should need to change between local and production is that DRIVE_DISK environment variable and the APIs between local and S3 will be the same (bar any advanced situations).

    Drive is a wrapper around FlyDrive specifically integrating FlyDrive with AdonisJS. Both, written and maintained by the AdonisJS core team.

  • Published lesson Creating A Lucid Model Mixin for our Organization Relationship

  • Published lesson Seeding Our Initial Database Data

  • Published lesson Typing Lucid Models in Inertia with DTOs

  • Replied to Hello everyone,I recently had trouble connecting to my database...

    Thanks for sharing Petzi!! 😊

  • Replied to Thanks a lot. It work now.I'm going to continue to watch the...

    You're welcome, Nico! Happy to hear all is working now!

    Hope you enjoy your time with AdonisJS & Edge! 😊

  • Replied to Thank you tom, it helped ! I'm actually following your courses...

    That's awesome to hear, Petzi! 😊

    Thank you for watching! I hope you enjoy your time working with AdonisJS!

  • Replied to Hey, first, thanks for this full tutorial.I try to use route...

    Hey Nico! Thanks for watching!

    When you're passing props into your button component, you're already inside of an EdgeJS interpolation area. So, at that point you don't need the double-curly braces and adding them will actually mess things up in that context. Instead, you can directly access the route function, like below.

    @shared.button({ href: route('movie.show', { id: movie.slug }), class: "" })
      View Details
    @end
    Copied!

    Also, and this might've just been formatting in the comment, but just in case also be sure the start and end tags for your EdgeJS components are on their own lines.

  • Upvoted discussion A Simple Framework Based on Edge.js

  • Upvoted discussion Which framework to start project ?

  • Replied to discussion Which framework to start project ?

    Hi Petzi!

    Real time data is going to be possible with either approach. As for which you should choose, that's really up to you. If you have a ton of dynamic elements, Inertia would probably be better. If it is mostly static content with some dynamic elements, then EdgeJS would likely be much easier.

    The best way to answer this is to get hands on experience with them yourself, as everybody's opinions are going to be different. I'd recommend spending about a half hour to an hour with each to get a feel for them to help aide in your decision.

    As for whether combining Inertia with Vue is a good choice, using Inertia would actually require working with Vue, React, Svelte, etc. Inertia is just a data broker between AdonisJS and those frontend client. If you choose to go with Inertia, which frontend client you choose out of those is completely up to you and your preferences. There's no superiority in the eyes of Inertia there. 😊

    Hope this helps!

  • Published lesson Understanding Our Database Schema

  • Published lesson Defining Our Migrations & Foreign Keys

  • Published lesson Defining Our Lucid Models & Relationships

  • Upvoted comment It works! Thanks!

  • Replied to It works! Thanks!

    Awesome to hear! Anytime!!

  • Replied to I am using AdonisJS 5 and using @inertia 0.11.1. I also enabled...

    In order to use TypeScript your Inertia code is going to need a separate tsconfig, as the one AdonisJS uses isn't going to be compatible with it.

    You may have luck trying to use one similar to the latest Inertia projects, or following an older guide we have on the subject.

    I'd personally recommend just getting onto the latest versions though, you're going to have a much easier time.

  • Replied to This is exactly what I need, did a marathon on this series and...

    Thanks so much, Fauzan!! I'm ecstatic to hear that! 😁

  • Replied to I should add I have installed @inertiajs/vue3 instead of '@inertiajs...

    Are you using AdonisJS 5 or AdonisJS 6? Conversely, are you using Inertia 1 or an older Inertia version?

    This series was released prior to Inertia 1's release and is meant for AdonisJS 5. The imports used throughout this series will reflect that accordingly.

    We're currently in the process of releasing an up-to-date Inertia series, called Building with AdonisJS & Inertia. If you're not working with an older project, I'd recommend following that series instead of this one.

  • Replied to I've found the same error running "node ace make:model movie...

    Hi Antoniogiroz! Please make sure you have Lucid installed and configure inside of your project.

    npm i @adonisjs/lucid
    Copied!

    Then

    node ace configure @adonisjs/lucid
    Copied!

    If you continue to have issues, please provide some more details or a repository with a reproduction of your issue.

  • Published snippet Accessing Lucid's Knex Connection Client

  • Published lesson The useForm Helper

  • Published lesson Common useForm Methods & Options

  • Published lesson Creating A FormInput Vue Component

  • Published lesson Cross-Site Request Forgery (CSRF) Protection in InertiaJS

  • Published lesson What Are Some of Inertia's Limitations

  • Replied to Thanks for the response! the differentiation you pointed out...

    Anytime!! Awesome, I'm happy to hear that! 😊 Should be discussed in the following two lessons:

    4.8 - Setting Up A ToastManager
    5.3 - Logging In Users & Displaying Exceptions

    *lesson numbers subject to change slightly

  • Replied to Hi, thank you from Italy for this tutorial!! I have problem ...

    Hi Davide! Rick Strahl's blog is a fantastic resource, especially for .NET devs! If you don't have any localhost projects making use of HSTS, you should be able to just clear out your browser's HSTS cache to get things working again. You can find AdonisJS' HSTS config within config/shield.ts.

    If however, you'd like to work with or need to work with HTTPS locally, you may have luck giving Caddy a try! I've never worked with it, but it automatically comes with and renews TLS certs for HTTPS and works on localhost.

  • Published lesson Specifying Page Titles & Meta Tags

  • Published lesson What Code Can & Can't Be Shared Between AdonisJS & Inertia

  • Published lesson Inertia Form Basics

  • Published lesson Form Validation & Displaying Errors

  • Replied to I believe it should be noted that errors: (ctx) =&gt; ctx.session...

    Hi Thenial! That is correct, errorsBag won't be included when you specifically get errors. I opted to save this discussion for a later lesson, but the way I like to think of it is:

    • errors holds your validation errors

    • errorsBag holds your exceptions

    The useForm helper is really only going to be concerned with validation errors, as it works best when they have a field key, and exceptions tend to be more generic and not specific to any one field. Later in this series, we'll watch for the E_INVALID_CREDENTIALS exception using an alert component. We'll also set up a ToastManager to automatically display a toast notification anytime an exception is reached. When we have validation errors, we'll also get an errorsBag entry with a summary of those validation errors, so a nice bonus of this approach is we'll also get a little toast when we have validation errors saying something like: "The form could not be saved. Please check the errors below."

    Additionally, the inputErrorsBag is, at this point, going to mostly be the same as errors. See:
    https://github.com/adonisjs/session/blob/develop/src/session.ts#L360-L370

  • Replied to Works perfectly, Thanks!

    Awesome!! Anytime!

  • Replied to Hi, thanks for the reply! and that makes sense. If I choose ...

    Anytime! Nope, I haven't tried it, but it should be perfectly fine! Just be sure to update the entry points within the below.

    • config/inertia.ts

    • resources/views/inertia_layout.edge

    • vite.config.ts

    In your config/inertia.ts, there's a property called entrypoint you'll want to add to the base of the config that points to wherever you're client-side entrypoint is. Then, if you're using SSR, you'll also want to update the one nested inside the ssr property of the config.

    I'd reckon it'd look something like:

    const inertiaConfig = defineConfig({
      // ... the other stuff
    
      entrypoint: 'resources/js/app.ts', // client-side entrypoint
      ssr: {
        enabled: true,
        entrypoint: 'resources/js/ssr.ts', // server-side entrypoint
      },
    })
    Copied!
  • Replied to Hi, what is the reason for having the inertia directory in the...

    Hi Thenial! I don't know the exact reason, this is a change that was made in v1.0.0-19 pre-release. I believe it was done for cleanliness reasons. Since, you can have Inertia and non-Inertia pages, mix-and-matching both with them all within resources can lead to things being a little difficult to sift through. Having them completely separated does ease that a bit.

    I would add an asterisks next to that section of the docs specifically for Inertia assets. 😉