studiocms
Version:
Astro Native CMS for AstroDB. Built from the ground up by the Astro community.
345 lines (344 loc) • 14.2 kB
JavaScript
import { eq, like } from "astro:db";
import { Effect, genLogger, pipe, Schema } from "../../../effect.js";
import { AstroDB } from "../effect/db.js";
import {
noUndefinedEntries,
parseData,
parsedDataResponse,
SelectPluginDataRespondOrFail
} from "../effect/pluginUtils.js";
import { tsPluginData } from "../tables.js";
import { CacheContext, isCacheEnabled, isCacheExpired } from "../utils.js";
class SDKCore_PLUGINS extends Effect.Service()(
"studiocms/sdk/SDKCore/modules/plugins",
{
dependencies: [AstroDB.Default],
effect: genLogger("studiocms/sdk/SDKCore/modules/plugins/effect")(function* () {
const [dbService, { pluginData }] = yield* Effect.all([AstroDB, CacheContext]);
const _db = {
/**
* Creates a batch request function for querying plugin data from the database.
*
* The returned function accepts an object with `batchSize` and `offset` properties,
* and performs a database query to select a batch of plugin data records with the specified
* limit and offset.
*/
batchRequest: dbService.makeQuery(
(query, o) => query((db) => db.select().from(tsPluginData).limit(o.batchSize).offset(o.offset))
),
/**
* Retrieves plugin data entries from the database whose IDs match the specified plugin ID prefix.
*/
getEntriesPluginData: dbService.makeQuery(
(query, pluginId) => query(
(db) => db.select().from(tsPluginData).where(like(tsPluginData.id, `${pluginId}-%`))
)
),
/**
* Executes a database query to select a single plugin data entry by its ID.
*/
selectPluginDataEntry: dbService.makeQuery(
(query, id) => query((db) => db.select().from(tsPluginData).where(eq(tsPluginData.id, id)).get())
),
/**
* Inserts a new plugin data entry into the database and returns the inserted record.
*/
insertPluginDataEntry: dbService.makeQuery(
(query, data) => query((db) => db.insert(tsPluginData).values(data).returning().get())
),
/**
* Updates an existing plugin data entry in the database.
*/
updatePluginDataEntry: dbService.makeQuery(
(query, data) => query(
(db) => db.update(tsPluginData).set(data).where(eq(tsPluginData.id, data.id)).returning().get()
)
)
};
const initPluginDataCache = Effect.fn(
"studiocms/sdk/SDKCore/modules/plugins/effect/initPluginDataCache"
)(function* (BATCH_SIZE) {
let batchSize = BATCH_SIZE || 100;
if (batchSize <= 0) {
batchSize = 100;
}
let offset = 0;
const sharedTimestamp = /* @__PURE__ */ new Date();
while (true) {
const entries = yield* _db.batchRequest({ batchSize, offset });
if (entries.length === 0) break;
const cacheEntries = entries.map(
(entry) => [entry.id, { data: entry, lastCacheUpdate: sharedTimestamp }]
);
for (const [id, cacheData] of cacheEntries) {
pluginData.set(id, cacheData);
}
offset += batchSize;
}
});
const clearPluginDataCache = () => Effect.try({
try: () => pluginData.clear(),
catch: () => new Error("Failed to clear plugin data cache")
});
const _selectPluginDataEntry = Effect.fn(
"studiocms/sdk/SDKCore/modules/plugins/effect/_selectPluginDataEntry"
)(function* (id) {
if (yield* isCacheEnabled) {
const cached = pluginData.get(id);
if (cached && !isCacheExpired(cached)) {
const { data: cacheData } = cached;
return cacheData;
}
const fresh = yield* _db.selectPluginDataEntry(id);
if (fresh) {
pluginData.set(id, {
data: fresh,
lastCacheUpdate: /* @__PURE__ */ new Date()
});
}
return fresh;
}
return yield* _db.selectPluginDataEntry(id);
});
const _insertPluginDataEntry = Effect.fn(
"studiocms/sdk/SDKCore/modules/plugins/effect/_insertPluginDataEntry"
)(function* (data) {
const newData = yield* _db.insertPluginDataEntry(data);
if (yield* isCacheEnabled) {
pluginData.set(newData.id, {
data: newData,
lastCacheUpdate: /* @__PURE__ */ new Date()
});
}
return newData;
});
const _updatePluginDataEntry = Effect.fn(
"studiocms/sdk/SDKCore/modules/plugins/effect/_updatePluginDataEntry"
)(function* (data) {
const updatedData = yield* _db.updatePluginDataEntry(data);
if (yield* isCacheEnabled) {
pluginData.set(updatedData.id, {
data: updatedData,
lastCacheUpdate: /* @__PURE__ */ new Date()
});
}
return updatedData;
});
const _processEntryFromCache = Effect.fn(function* ([key, { data: entry, lastCacheUpdate }], pluginId, validator) {
if (!key.startsWith(pluginId)) {
return void 0;
}
if ((yield* isCacheEnabled) && isCacheExpired({ lastCacheUpdate })) {
const freshEntry = yield* _db.selectPluginDataEntry(entry.id);
if (!freshEntry) {
yield* Effect.log(`Removing stale cache entry: ${entry.id}`);
pluginData.delete(entry.id);
return void 0;
}
const validated2 = yield* parseData(freshEntry.data, validator);
pluginData.set(entry.id, {
data: freshEntry,
lastCacheUpdate: /* @__PURE__ */ new Date()
});
return yield* parsedDataResponse(entry.id, validated2);
}
const validated = yield* parseData(entry.data, validator);
return yield* parsedDataResponse(entry.id, validated);
});
const _processEntryFromDB = Effect.fn(function* (entry, validator) {
const validated = yield* parseData(entry.data, validator);
if (yield* isCacheEnabled) {
pluginData.set(entry.id, {
data: entry,
lastCacheUpdate: /* @__PURE__ */ new Date()
});
}
return yield* parsedDataResponse(entry.id, validated);
});
const _getEntries = Effect.fn("studiocms/sdk/SDKCore/modules/plugins/effect/_getEntries")(
function* (pluginId, validator, filter) {
if (yield* isCacheEnabled) {
const data = yield* pipe(
pluginData.entries(),
Effect.forEach((entry) => _processEntryFromCache(entry, pluginId, validator)),
Effect.map(noUndefinedEntries)
);
if (data.length > 0) return data;
}
const todo = pipe(
_db.getEntriesPluginData(pluginId),
Effect.flatMap(Effect.forEach((entry) => _processEntryFromDB(entry, validator)))
);
if (filter) return yield* todo.pipe(Effect.map(filter));
return yield* todo;
}
);
const _selectPluginDataEntryRespondOrFail = Effect.fn(
"studiocms/sdk/SDKCore/modules/plugins/effect/_selectPluginDataEntryRespondOrFail"
)(function* (id, mode) {
const existing = yield* _selectPluginDataEntry(id);
switch (mode) {
case SelectPluginDataRespondOrFail.ExistsNoFail: {
if (existing) return existing;
return void 0;
}
case SelectPluginDataRespondOrFail.ExistsShouldFail: {
if (existing)
return yield* Effect.fail(new Error(`Plugin data with ID ${id} already exists.`));
return void 0;
}
case SelectPluginDataRespondOrFail.NotExistsShouldFail: {
if (!existing)
return yield* Effect.fail(new Error(`Plugin data with ID ${id} does not exist.`));
return void 0;
}
default:
return yield* Effect.fail(new Error(`Invalid mode: ${mode}`));
}
});
const _select = Effect.fn(
"studiocms/sdk/SDKCore/modules/plugins/effect/usePluginData.select"
)(function* (generatedEntryId, validator) {
const existing = yield* _selectPluginDataEntryRespondOrFail(
generatedEntryId,
SelectPluginDataRespondOrFail.ExistsNoFail
);
if (!existing) return void 0;
const data = yield* parseData(existing.data, validator);
return yield* parsedDataResponse(generatedEntryId, data);
});
const _insert = Effect.fn(
"studiocms/sdk/SDKCore/modules/plugins/effect/usePluginData.insert"
)(function* (generatedEntryId, data, validator) {
yield* _selectPluginDataEntryRespondOrFail(
generatedEntryId,
SelectPluginDataRespondOrFail.ExistsShouldFail
);
const parsedData = yield* parseData(data, validator);
const inserted = yield* _insertPluginDataEntry({
id: generatedEntryId,
data: parsedData
});
return yield* parsedDataResponse(inserted.id, parsedData);
});
const _update = Effect.fn(
"studiocms/sdk/SDKCore/modules/plugins/effect/usePluginData.update"
)(function* (generatedEntryId, data, validator) {
yield* _selectPluginDataEntryRespondOrFail(
generatedEntryId,
SelectPluginDataRespondOrFail.NotExistsShouldFail
);
const parsedData = yield* parseData(data, validator);
const updated = yield* _updatePluginDataEntry({
id: generatedEntryId,
data: parsedData
});
return yield* parsedDataResponse(updated.id, parsedData);
});
const buildReturn = (pluginId, entryId, validator) => {
const generatedEntryId = `${pluginId}-${entryId}`;
return {
/**
* Generates a unique ID for the plugin data entry.
*
* @returns An Effect that yields the generated ID. In the format `${pluginId}-${entryId}`
*/
generatedId: () => Effect.succeed(generatedEntryId),
/**
* Selects a plugin data entry by its ID, validating the data if a validator is provided.
*
* @returns An Effect that yields the selected plugin data entry or `undefined` if not found.
*/
select: () => _select(generatedEntryId, validator),
/**
* Inserts new plugin data into the database after validating the input.
*
* @param data - The plugin data to insert.
* @yields Throws an error if validation fails or if the entry already exists.
* @returns The parsed data response for the inserted entry.
*/
insert: (data) => _insert(generatedEntryId, data, validator),
/**
* Updates existing plugin data in the database after validating the input.
*
* @param data - The updated plugin data.
* @yields Throws an error if validation fails.
* @returns The parsed data response for the updated entry.
*/
update: (data) => _update(generatedEntryId, data, validator)
};
};
function usePluginData(pluginId, { entryId, validator } = {}) {
if (!entryId) {
return {
/**
* Retrieves all plugin data entries for the specified plugin ID.
*
* @template T - The type of the plugin data object.
* @param validator - Optional validator options for validating the plugin data.
* @returns An Effect that yields an array of `PluginDataEntry<T>` objects.
*/
getEntries: (filter) => _getEntries(pluginId, validator, filter),
getEntry: (id) => buildReturn(pluginId, id, validator)
};
}
return buildReturn(pluginId, entryId, validator);
}
class InferType {
_Schema;
$UsePluginData;
$Insert;
constructor(schema) {
if (!schema || !Schema.isSchema(schema)) {
throw new Error("InferType requires a valid Schema.Struct instance.");
}
this._Schema = schema;
}
}
return {
/**
* Provides a set of effectful operations for managing plugin data entries by plugin ID and optional entry ID.
*
* When an `entryId` is provided, returns an object with methods to:
* - Generate a unique plugin data entry ID.
* - Insert new plugin data after validation and duplicate checks.
* - Select and validate existing plugin data by ID.
* - Update existing plugin data after validation.
*
* When no `entryId` is provided, returns an object with a method to retrieve all entries for the given plugin.
*
* @param pluginId - The unique identifier for the plugin.
* @param entryId - (Optional) The unique identifier for the plugin data entry.
* @returns An object with effectful methods for plugin data management, varying by presence of `entryId`.
*/
usePluginData,
/**
* Initializes the plugin data cache by fetching all existing entries from the database
* and populating the in-memory cache with these entries.
*/
initPluginDataCache,
/**
* Clears the plugin data cache, removing all cached entries.
*
* @returns An Effect that resolves to `void` on success or an `Error` on failure.
*/
clearPluginDataCache,
/**
* Utility class to infer types from a given Schema.
*
* @typeParam S - The schema type extending `Schema.Struct<any>`.
*
* @property _Schema - The schema instance used for type inference.
* @property usePluginData - The inferred type from the schema, used for plugin data.
* @property Insert - A recursively simplified, mutable version of the schema's type.
*
*/
InferType
};
})
}
) {
}
export {
SDKCore_PLUGINS
};