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.
- package/README.md +5 -0
- package/bin/commands/db-scaffold.js +81 -0
- package/bin/utils/model-migrations.js +211 -0
- package/bin/webspresso.js +2 -0
- package/core/content/cache.js +64 -0
- package/core/content/field-types.js +180 -0
- package/core/content/index.js +30 -0
- package/core/content/renderer.js +84 -0
- package/core/content/schema.js +75 -0
- package/core/content/service.js +400 -0
- package/core/content/types.js +59 -0
- package/index.d.ts +17 -0
- package/index.js +7 -0
- package/package.json +1 -1
- package/plugins/admin-panel/app.js +7 -7
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +41 -0
- package/plugins/admin-panel/client/parts/06-login-setup-forms.js +2 -2
- package/plugins/admin-panel/index.js +17 -18
- package/plugins/admin-panel/modules/menu.js +1 -0
- package/plugins/content/admin/content-entries-component.js +291 -0
- package/plugins/content/admin/content-types-component.js +250 -0
- package/plugins/content/api-handlers.js +157 -0
- package/plugins/content/client/inline-edit.css +296 -0
- package/plugins/content/client/inline-edit.js +366 -0
- package/plugins/content/helpers.js +77 -0
- package/plugins/content/index.js +231 -0
- package/plugins/content/migration-template.js +54 -0
- package/plugins/content/models/content-entry.js +45 -0
- package/plugins/content/models/content-type.js +36 -0
- package/plugins/index.js +2 -0
- package/src/file-router.js +21 -1
- package/templates/skills/webspresso-usage/REFERENCE-framework.md +1 -1
- 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
|
|
package/src/file-router.js
CHANGED
|
@@ -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
|
-
|
|
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).
|