Skip to content

Server reference (@martipops/cms-adonis) ​

Setup ​

PieceWhere
Provideradonisrc.ts → () => import('@martipops/cms-adonis/cms_provider')
Configconfig/cms.ts (below)
Routesrouter.group(() => registerCmsRoutes(router)).prefix('admin/cms').use(middleware.auth())
MigrationCreates cms_sections, cms_documents, cms_revisions, cms_seeds, or the same with your tablePrefix (see installing)
Serviceimport cms from '@martipops/cms-adonis/services/main'

config/cms.ts ​

ts
import { defineConfig } from '@martipops/cms-adonis'

export default defineConfig({
  pages: [site, home, about], // definePage results
  collections: [locations, specials], // defineCollection results
  fieldTypes: [weeklyHoursType], // defineFieldType results

  authorize: (ctx) => ctx.bouncer?.allows('manageContent') ?? false,
  user: ({ auth }) => (auth?.user ? { id: auth.user.id, name: auth.user.fullName } : null),
  media: {
    exists: async (key, folder) => Boolean(await Media.query().where({ key, folder }).first()),
    baseUrl: '/media/', // optional; this is the default
  },
})
OptionRequiredPurpose
pagesEditable pages and site-wide scopes
collectionsEditable collections
fieldTypesCustom field types used by f.custom
authorize✓Whether a request may edit. It gates the endpoints and whether schemas are sent. Memoized per request
userWho to record on revisions ({ id, name }). The name is copied into the revision, so history survives deleted users
mediaThe media library. Required as soon as a schema has f.image or f.file (checked at boot)
media.exists(key, folder) => Promise<boolean>. Validates f.image / f.file values
media.baseUrlPublic base URL of media files, default /media/. Links and URLs under it count as uses of that file
tablePrefixPrefix of the CMS tables, default cms_. Must match the migration. cmsTables(prefix) gives the names

All schemas are compiled when the service boots, so a typo in a schema or an unknown custom type fails at startup.

The Cms service ​

Reading ​

MethodReturns
pageProps(name, ctx)PageProps: resolved values, plus schema for editors
collectionProps<V>(name, ctx)CollectionProps<V>: items (hidden ones for editors), plus schema for editors
sections(name)Resolved values per section (cached)
items<V>(name, { includeHidden? })Document<V>[] in display order
find<V>(name, slug)A visible item, or null
findOrFail<V>(name, slug)A visible item, or a 404 exception
canEdit(ctx)authorize(ctx), memoized per request
page(name) / collection(name)The definition (404 exception if unknown)
ts
// A controller
const location = await cms.findOrFail<LocationData>('locations', params.slug)

// A validator rule
if (!(await cms.find('locations', value)))
  field.report('Choose one of our locations', 'location', field)

Writing ​

MethodNotes
commit(ops, user)Applies ops atomically. Throws CommitValidationError (.errors) on any invalid op. See protocol.md
history(target)Last 25 revisions of { page, section } or { collection, id }

Server code can call commit directly, for imports, scripts or tests, and gets the same validation and history as the editor.

Media ​

MethodReturns
defaultMediaKeys()Keys referenced by schema defaults and collection initial data (static)
mediaKeysInUse()The above plus every key in saved sections and documents

The app's media library calls these to refuse deleting files the site uses.

Housekeeping ​

clearCache() drops cached sections and documents and the "already seeded" memo. Tests call it in group.each.setup, because rolled-back transactions would otherwise leave stale cache entries.

HTTP endpoints ​

Registered by registerCmsRoutes(router). They check authorize themselves, so they're safe even if a group's middleware is misconfigured.

RouteNameResponse
POST commitcms.commit200 { created } · 422 { message, errors } · 403
GET history?page=&section=cms.history200 { data: Revision[] }
GET history?collection=&id=cms.historysame

The commit endpoint parses the raw JSON body. Adonis's bodyparser setting convertEmptyStringsToNull would otherwise turn a deliberately cleared text field into null.

Tables ​

Create them from a migration that calls the package's versioned schema step. A released version never changes; a future schema change ships as cmsTablesV2, added as a new migration:

ts
import { BaseSchema } from '@adonisjs/lucid/schema'
import { cmsTablesV1 } from '@martipops/cms-adonis/schema'

export default class extends BaseSchema {
  async up() {
    cmsTablesV1.up(this.schema) // or (this.schema, 'site_') to match tablePrefix
  }
  async down() {
    cmsTablesV1.down(this.schema)
  }
}
TableKey columnsNotes
cms_sectionspage, section (unique together), data JSONOnly edited fields. The row is deleted when all fields are reset
cms_documentscollection, slug (unique together), data JSON, sort_order, hiddenIndex on (collection, sort_order)
cms_revisionskind (section/document), target (home.hero / locations:12), action, data, hidden, user_id, user_nameAppend-only. Index on (kind, target)
cms_seedscollection (primary key), seeded_atPresent = initial already inserted

User columns are plain strings with no foreign key, so the package makes no assumption about the app's users table.

Testing ​

ts
import cms from '@martipops/cms-adonis/services/main'
import { Cms, CommitValidationError, defineConfig } from '@martipops/cms-adonis'

test.group('…', (group) => {
  group.each.setup(() => {
    cms.clearCache()
    return testUtils.db().withGlobalTransaction()
  })
})
  • To test the library in isolation, build a new Cms(defineConfig({ … })) with throwaway schemas, as the package's own suite does (packages/cms-adonis/tests/cms.spec.ts). It boots a minimal app on in-memory SQLite (tests/app.ts) and needs nothing from the site.
  • An app's tests usually share its dev SQLite file. There, don't assert absolute row counts in shared tables; scope assertions to what the test created.
  • ⚠️ A global transaction hides connection problems, because everything runs on one connection. Code that opens its own connection inside a transaction can pass the tests and deadlock in production. The two-pass commit exists because of exactly this. Hit the real endpoint before you trust a change to the write path.