webspresso 0.0.77 → 0.0.78

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +5 -0
  2. package/bin/commands/db-scaffold.js +81 -0
  3. package/bin/utils/model-migrations.js +211 -0
  4. package/bin/webspresso.js +2 -0
  5. package/core/content/cache.js +64 -0
  6. package/core/content/field-types.js +180 -0
  7. package/core/content/index.js +30 -0
  8. package/core/content/renderer.js +84 -0
  9. package/core/content/schema.js +75 -0
  10. package/core/content/service.js +400 -0
  11. package/core/content/types.js +59 -0
  12. package/index.d.ts +17 -0
  13. package/index.js +7 -0
  14. package/package.json +1 -1
  15. package/plugins/admin-panel/app.js +7 -7
  16. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +41 -0
  17. package/plugins/admin-panel/client/parts/06-login-setup-forms.js +2 -2
  18. package/plugins/admin-panel/index.js +17 -18
  19. package/plugins/admin-panel/modules/menu.js +1 -0
  20. package/plugins/content/admin/content-entries-component.js +291 -0
  21. package/plugins/content/admin/content-types-component.js +250 -0
  22. package/plugins/content/api-handlers.js +157 -0
  23. package/plugins/content/client/inline-edit.css +296 -0
  24. package/plugins/content/client/inline-edit.js +366 -0
  25. package/plugins/content/helpers.js +77 -0
  26. package/plugins/content/index.js +231 -0
  27. package/plugins/content/migration-template.js +54 -0
  28. package/plugins/content/models/content-entry.js +45 -0
  29. package/plugins/content/models/content-type.js +36 -0
  30. package/plugins/index.js +2 -0
  31. package/src/file-router.js +21 -1
  32. package/templates/skills/webspresso-usage/REFERENCE-framework.md +1 -1
  33. package/templates/skills/webspresso-usage/SKILL.md +5 -0
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Migration template for content plugin tables.
3
+ * @module plugins/content/migration-template
4
+ */
5
+
6
+ /**
7
+ * @param {string} [typesTable='content_types']
8
+ * @param {string} [entriesTable='content_entries']
9
+ * @returns {string}
10
+ */
11
+ function generateContentMigrations(typesTable = 'content_types', entriesTable = 'content_entries') {
12
+ return `/**
13
+ * Migration: Create content_types and content_entries tables
14
+ * Generated by content plugin — customize table names if needed.
15
+ */
16
+
17
+ exports.up = async function (knex) {
18
+ await knex.schema.createTable('${typesTable}', (table) => {
19
+ table.increments('id').primary();
20
+ table.string('slug', 128).notNullable().unique();
21
+ table.string('name', 255).notNullable();
22
+ table.text('description').nullable();
23
+ table.json('schema').notNullable();
24
+ table.json('settings').nullable();
25
+ table.timestamp('created_at').defaultTo(knex.fn.now()).notNullable();
26
+ table.timestamp('updated_at').defaultTo(knex.fn.now()).notNullable();
27
+ });
28
+
29
+ await knex.schema.createTable('${entriesTable}', (table) => {
30
+ table.increments('id').primary();
31
+ table.integer('content_type_id').unsigned().notNullable()
32
+ .references('id').inTable('${typesTable}').onDelete('CASCADE');
33
+ table.string('slug', 128).notNullable();
34
+ table.string('title', 255).nullable();
35
+ table.json('data').notNullable();
36
+ table.string('status', 32).notNullable().defaultTo('published');
37
+ table.string('locale', 16).nullable();
38
+ table.integer('revision').notNullable().defaultTo(1);
39
+ table.timestamp('created_at').defaultTo(knex.fn.now()).notNullable();
40
+ table.timestamp('updated_at').defaultTo(knex.fn.now()).notNullable();
41
+
42
+ table.unique(['content_type_id', 'slug', 'locale']);
43
+ table.index(['content_type_id', 'status']);
44
+ });
45
+ };
46
+
47
+ exports.down = async function (knex) {
48
+ await knex.schema.dropTableIfExists('${entriesTable}');
49
+ await knex.schema.dropTableIfExists('${typesTable}');
50
+ };
51
+ `;
52
+ }
53
+
54
+ module.exports = { generateContentMigrations };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * ContentEntry model — content entries with JSON field data.
3
+ * @module plugins/content/models/content-entry
4
+ */
5
+
6
+ const { defineModel } = require('../../../core/orm/model');
7
+ const { zdb } = require('../../../core/orm');
8
+
9
+ const ContentEntrySchema = zdb.schema({
10
+ id: zdb.id(),
11
+ content_type_id: zdb.foreignKey('content_types', { index: true }),
12
+ slug: zdb.string({ maxLength: 128, index: true }),
13
+ title: zdb.string({ maxLength: 255, nullable: true }),
14
+ data: zdb.json(),
15
+ status: zdb.enum(['published', 'draft'], { default: 'published', index: true }),
16
+ locale: zdb.string({ maxLength: 16, nullable: true, index: true }),
17
+ revision: zdb.integer({ default: 1 }),
18
+ created_at: zdb.timestamp({ auto: 'create' }),
19
+ updated_at: zdb.timestamp({ auto: 'update' }),
20
+ });
21
+
22
+ /**
23
+ * @returns {import('../../../core/orm/types').ModelDefinition}
24
+ */
25
+ function createContentEntryModel() {
26
+ return defineModel({
27
+ name: 'ContentEntry',
28
+ table: 'content_entries',
29
+ schema: ContentEntrySchema,
30
+ scopes: { timestamps: true },
31
+ admin: { enabled: false },
32
+ relations: {
33
+ contentType: {
34
+ type: 'belongsTo',
35
+ model: () => require('../../../core/orm/model').getModel('ContentType'),
36
+ foreignKey: 'content_type_id',
37
+ },
38
+ },
39
+ });
40
+ }
41
+
42
+ module.exports = {
43
+ createContentEntryModel,
44
+ ContentEntrySchema,
45
+ };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * ContentType model — schema-driven content type definitions.
3
+ * @module plugins/content/models/content-type
4
+ */
5
+
6
+ const { defineModel } = require('../../../core/orm/model');
7
+ const { zdb } = require('../../../core/orm');
8
+
9
+ const ContentTypeSchema = zdb.schema({
10
+ id: zdb.id(),
11
+ slug: zdb.string({ unique: true, maxLength: 128, index: true }),
12
+ name: zdb.string({ maxLength: 255 }),
13
+ description: zdb.text({ nullable: true }),
14
+ schema: zdb.json(),
15
+ settings: zdb.json({ nullable: true }),
16
+ created_at: zdb.timestamp({ auto: 'create' }),
17
+ updated_at: zdb.timestamp({ auto: 'update' }),
18
+ });
19
+
20
+ /**
21
+ * @returns {import('../../../core/orm/types').ModelDefinition}
22
+ */
23
+ function createContentTypeModel() {
24
+ return defineModel({
25
+ name: 'ContentType',
26
+ table: 'content_types',
27
+ schema: ContentTypeSchema,
28
+ scopes: { timestamps: true },
29
+ admin: { enabled: false },
30
+ });
31
+ }
32
+
33
+ module.exports = {
34
+ createContentTypeModel,
35
+ ContentTypeSchema,
36
+ };
package/plugins/index.js CHANGED
@@ -21,6 +21,7 @@ const { uploadPlugin, createLocalFileProvider } = require('./upload');
21
21
  const { dataExchangePlugin } = require('./data-exchange');
22
22
  const { redirectPlugin } = require('./redirect');
23
23
  const { rateLimitPlugin } = require('./rate-limit');
24
+ const contentPlugin = require('./content');
24
25
 
25
26
  module.exports = {
26
27
  sitemapPlugin,
@@ -41,5 +42,6 @@ module.exports = {
41
42
  dataExchangePlugin,
42
43
  redirectPlugin,
43
44
  rateLimitPlugin,
45
+ contentPlugin,
44
46
  };
45
47
 
@@ -810,6 +810,13 @@ function mountPages(app, options) {
810
810
  // Create context with plugin helpers merged
811
811
  const baseHelpers = createHelpers({ req, res, locale });
812
812
  const pluginHelpers = pluginManager ? pluginManager.getHelpers() : {};
813
+ if (pluginManager) {
814
+ const contentApi = pluginManager.getPluginAPI('content');
815
+ if (contentApi?.createRequestHelpers) {
816
+ const buildHelpers = contentApi.createRequestHelpers();
817
+ pluginHelpers.content = buildHelpers(req);
818
+ }
819
+ }
813
820
 
814
821
  const njkTpl = loadNjkRouteTemplate(route.fullPath, isDev);
815
822
 
@@ -863,6 +870,12 @@ function mountPages(app, options) {
863
870
  await executeHook(routeHooks, 'afterMiddleware', ctx);
864
871
 
865
872
  // Execute hooks: beforeLoad
873
+ if (pluginManager && db) {
874
+ const contentApi = pluginManager.getPluginAPI('content');
875
+ if (contentApi?.getContentService) {
876
+ ctx.content = contentApi.getContentService(db);
877
+ }
878
+ }
866
879
  await executeHook(globalHooks, 'beforeLoad', ctx);
867
880
  await executeHook(routeHooks, 'beforeLoad', ctx);
868
881
 
@@ -908,10 +921,17 @@ function mountPages(app, options) {
908
921
 
909
922
  // Render the template
910
923
  const templatePath = route.file.split(path.sep).join('/');
911
- const html =
924
+ let html =
912
925
  njkTpl.useStringRender && njkTpl.templateBody != null
913
926
  ? nunjucks.renderString(njkTpl.templateBody, renderContext, { path: route.fullPath })
914
927
  : nunjucks.render(templatePath, renderContext);
928
+
929
+ if (pluginManager) {
930
+ const contentApi = pluginManager.getPluginAPI('content');
931
+ if (contentApi?.maybeInjectInlineEdit) {
932
+ html = contentApi.maybeInjectInlineEdit(req, html);
933
+ }
934
+ }
915
935
 
916
936
  // Execute hooks: afterRender
917
937
  ctx.html = html;
@@ -178,7 +178,7 @@ Analytics plugin adds `fsy.analyticsHead`, `fsy.verificationTags`, etc., when co
178
178
 
179
179
  **Query builder:** `UserRepo.query().where(...).with('relation').orderBy(...).list()` / `.first()` / `.paginate()` / `.count()`. **`with()`** eager-loads relations; **`count()`** ignores builder `.limit`/`.offset` for total; see ORM docs for edge cases.
180
180
 
181
- **Migrations:** `webspresso db:migrate`, `db:rollback`, `db:status`, `db:make`.
181
+ **Migrations:** `webspresso db:migrate`, `db:rollback`, `db:status`, `db:make`, `db:scaffold` (all models → create migrations).
182
182
 
183
183
  **Transactions:** `db.transaction(async (trx) => { trx.getRepository('User') })`.
184
184
 
@@ -21,6 +21,7 @@ There are **three** files in this folder; open the reference that matches the ta
21
21
  | Greenfield project, `webspresso new`, `dev`, `db:migrate`, `doctor` | [`REFERENCE-framework.md`](./REFERENCE-framework.md) § CLI | Full command table there. |
22
22
  | Event bus, `kernel.createApp`, `defineFlow`, `definePlugin`, simulated `BaseRepository`, namespaced minimal views | [`REFERENCE-kernel.md`](./REFERENCE-kernel.md) | **Not** SSR **`createApp`** — [`doc/index.html#application-kernel`](../../../doc/index.html#application-kernel). |
23
23
  | Long-form narrative, all options | [`README.md`](../../../README.md), [`doc/index.html`](../../../doc/index.html) | Single-page HTML docs. |
24
+ | System structure, feature sets, C4 map | [`docs/ARCHITECTURE.md`](../../../docs/ARCHITECTURE.md) | **Update when adding plugins, CLI commands, or extension points.** |
24
25
 
25
26
  ## CLI cheat sheet
26
27
 
@@ -40,3 +41,7 @@ There are **three** files in this folder; open the reference that matches the ta
40
41
  - **`REFERENCE-kernel.md`:** `kernel`, domain events, `dispatch` / `publish`, `registerFlow`, `definePlugin` inside the kernel.
41
42
 
42
43
  They are independent: most apps only need SSR **`createApp`**; the kernel is optional.
44
+
45
+ ## Architecture maintenance
46
+
47
+ When you add or materially change a **plugin**, **CLI command**, **extension point**, or **request-pipeline rule**, update [`docs/ARCHITECTURE.md`](../../../docs/ARCHITECTURE.md) (feature-set section + changelog).